From 43e6e16168a7b7bc6e07e46396da1c26e524dc47 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 14:08:37 +0200 Subject: [PATCH 001/206] Implemented PAC and Midrow code handling for EIA-608 captions --- .../exoplayer2/text/eia608/Eia608Decoder.java | 316 ++++++++++++++++-- .../text/eia608/Eia608Subtitle.java | 18 +- 2 files changed, 292 insertions(+), 42 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index b5249cde78..c54cc2ef5e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -15,14 +15,27 @@ */ package com.google.android.exoplayer2.text.eia608; -import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; + +import java.util.HashMap; import java.util.LinkedList; import java.util.TreeSet; @@ -86,6 +99,12 @@ public final class Eia608Decoder implements SubtitleDecoder { private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; + private static final byte CTRL_TAB_OFFSET_CHAN_1 = 0x17; + private static final byte CTRL_TAB_OFFSET_CHAN_2 = 0x1F; + private static final byte CTRL_TAB_OFFSET_1 = 0x21; + private static final byte CTRL_TAB_OFFSET_2 = 0x22; + private static final byte CTRL_TAB_OFFSET_3 = 0x23; + private static final byte CTRL_BACKSPACE = 0x21; private static final byte CTRL_MISC_CHAN_1 = 0x14; @@ -159,13 +178,66 @@ public final class Eia608Decoder implements SubtitleDecoder { 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 }; + // Maps EIA-608 PAC row numbers to WebVTT cue line settings. + // Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac + private static final float[] CUE_LINE_MAP = new float[] { + 10.00f, // Row 1 + 15.33f, + 20.66f, + 26.00f, + 31.33f, + 36.66f, + 42.00f, + 47.33f, + 52.66f, + 58.00f, + 63.33f, + 68.66f, + 74.00f, + 79.33f, + 84.66f // Row 15 + }; + + // Maps EIA-608 PAC indents to WebVTT cue position values. + // Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac + // Note that these cue position values may not give the intended result, unless the font size is set + // to allow for a maximum of 32 (or 41) characters per line. + private static final float[] INDENT_MAP = new float[] { + 10.0f, // Indent 0/Column 1 + 20.0f, // Indent 4/Column 5 + 30.0f, // Indent 8/Column 9 + 40.0f, // Indent 12/Column 13 + 50.0f, // Indent 16/Column 17 + 60.0f, // Indent 20/Column 21 + 70.0f, // Indent 24/Column 25 + 80.0f, // Indent 28/Column 29 + }; + + private static final int[] COLOR_MAP = new int[] { + Color.WHITE, + Color.GREEN, + Color.BLUE, + Color.CYAN, + Color.RED, + Color.YELLOW, + Color.MAGENTA, + Color.BLACK // Only used by Mid Row style changes, for PAC an value of 0x7 means italics. + }; + + // Transparency is defined in the two left most bytes of an integer. + private static final int TRANSPARENCY_MASK = 0x80FFFFFF; + + private static final int STYLE_ITALIC = Typeface.ITALIC; + private static final float DEFAULT_CUE_LINE = CUE_LINE_MAP[10]; // Row 11 + private static final float DEFAULT_INDENT = INDENT_MAP[0]; // Indent 0 + private final LinkedList availableInputBuffers; private final LinkedList availableOutputBuffers; private final TreeSet queuedInputBuffers; private final ParsableByteArray ccData; - private final StringBuilder captionStringBuilder; + private final SpannableStringBuilder captionStringBuilder; private long playbackPositionUs; @@ -173,9 +245,12 @@ public final class Eia608Decoder implements SubtitleDecoder { private int captionMode; private int captionRowCount; - private String captionString; - private String lastCaptionString; + private LinkedList cues; + private HashMap captionStyles; + float cueIndent; + float cueLine; + int tabOffset; private boolean repeatableControlSet; private byte repeatableControlCc1; @@ -194,10 +269,14 @@ public final class Eia608Decoder implements SubtitleDecoder { ccData = new ParsableByteArray(); - captionStringBuilder = new StringBuilder(); + captionStringBuilder = new SpannableStringBuilder(); + captionStyles = new HashMap<>(); setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; + cueIndent = DEFAULT_INDENT; + cueLine = DEFAULT_CUE_LINE; + tabOffset = 0; } @Override @@ -253,11 +332,11 @@ public final class Eia608Decoder implements SubtitleDecoder { decode(inputBuffer); // check if we have any caption updates to report - if (!TextUtils.equals(captionString, lastCaptionString)) { - lastCaptionString = captionString; + if (!cues.isEmpty()) { if (!inputBuffer.isDecodeOnly()) { SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(captionString), 0); + outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(cues), 0); + cues = new LinkedList<>(); releaseInputBuffer(inputBuffer); return outputBuffer; } @@ -284,9 +363,11 @@ public final class Eia608Decoder implements SubtitleDecoder { setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; playbackPositionUs = 0; - captionStringBuilder.setLength(0); - captionString = null; - lastCaptionString = null; + flushCaptionBuilder(); + cues = new LinkedList<>(); + cueIndent = DEFAULT_INDENT; + cueLine = DEFAULT_CUE_LINE; + tabOffset = 0; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -342,6 +423,11 @@ public final class Eia608Decoder implements SubtitleDecoder { continue; } + // Mid row changes. + if ((ccData1 == 0x11 || ccData1 == 0x19) && ccData2 >= 0x20 && ccData2 <= 0x2F) { + handleMidrowCode(ccData1, ccData2); + } + // Control character. if (ccData1 < 0x20) { isRepeatableControl = handleCtrl(ccData1, ccData2); @@ -360,7 +446,7 @@ public final class Eia608Decoder implements SubtitleDecoder { repeatableControlSet = false; } if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionString = getDisplayCaption(); + buildCue(); } } } @@ -380,8 +466,9 @@ public final class Eia608Decoder implements SubtitleDecoder { if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { - // TODO: Add better handling of this with specific positioning. - maybeAppendNewline(); + handlePreambleCode(cc1, cc2); + } else if (isTabOffset(cc1, cc2)) { + handleTabOffset(cc2); } return isRepeatableControl; } @@ -414,32 +501,197 @@ public final class Eia608Decoder implements SubtitleDecoder { switch (cc2) { case CTRL_ERASE_DISPLAYED_MEMORY: - captionString = null; if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionStringBuilder.setLength(0); + flushCaptionBuilder(); } return; case CTRL_ERASE_NON_DISPLAYED_MEMORY: - captionStringBuilder.setLength(0); + flushCaptionBuilder(); return; case CTRL_END_OF_CAPTION: - captionString = getDisplayCaption(); - captionStringBuilder.setLength(0); + buildCue(); + flushCaptionBuilder(); return; case CTRL_CARRIAGE_RETURN: maybeAppendNewline(); return; case CTRL_BACKSPACE: - if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); - } + backspace(); return; } } + private void handlePreambleCode(byte cc1, byte cc2) { + // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands + applySpan(); // Apply any spans. + + // Parse the "next row down" flag. + boolean nextRowDown = (cc2 & 0x20) != 0; + if (nextRowDown) { + // TODO: We should create a new cue instead, this may cause issues when + // the new line receives it's own PAC which we ignore currently. + // As a result of that the new line will be positioned directly below the + // previous line. + maybeAppendNewline(); + } + + // Go through the bits, starting with the last bit - the underline flag: + boolean underline = (cc2 & 0x1) != 0; + if (underline) { + captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + } + + // Next, parse the attribute bits: + int attribute = cc2 >> 1 & 0xF; + if (attribute >= 0x0 && attribute < 0x7) { + // Attribute is a foreground color + captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute])); + } else if (attribute == 0x7) { + // Attribute is "italics" + captionStyles.put(getSpanStartIndex(), new StyleSpan(STYLE_ITALIC)); + } else if (attribute >= 0x8 && attribute <= 0xF) { + // Attribute is an indent + if (cueIndent == DEFAULT_INDENT) { + // Only update the indent, if it's the default indent. + // This is not conform the spec, but otherwise indentations may be off + // because we don't create a new cue when we see the nextRowDown flag. + cueIndent = INDENT_MAP[attribute & 0x7]; + } + } + + // Parse the row bits + int row = cc1 & 0x7; + if (row >= 0x4) { + // Extended Preamble Code + row = row & 0x3; + switch (row) { + case 0x0: + // Row 14 or 15 + cueLine = CUE_LINE_MAP[13]; + break; + case 0x1: + // Row 5 or 6 + cueLine = CUE_LINE_MAP[4]; + break; + case 0x2: + // Row 7 or 8 + cueLine = CUE_LINE_MAP[7]; + break; + case 0x3: + // Row 9 or 10 + cueLine = CUE_LINE_MAP[8]; + break; + } + } else { + // Regular Preamble Code + switch (row) { + case 0x0: + // Row 11 (Default) + cueLine = CUE_LINE_MAP[10]; + break; + case 0x1: + // Row 1 (Top) + cueLine = CUE_LINE_MAP[0]; + break; + case 0x2: + // Row 4 (Top) + cueLine = CUE_LINE_MAP[3]; + break; + case 0x3: + // Row 12 or 13 (Bottom) + cueLine = CUE_LINE_MAP[11]; + break; + } + } + } + + private void handleMidrowCode(byte cc1, byte cc2) { + boolean transparentOrUnderline = (cc2 & 0x1) != 0; + int attribute = cc2 >> 1 & 0xF; + if ((cc1 & 0x1) != 0) { + // Background Color + captionStyles.put(getSpanStartIndex(), new BackgroundColorSpan(transparentOrUnderline ? + COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); + } else { + // Foreground color + captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute])); + if (transparentOrUnderline) { + // Text should be underlined + captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + } + } + } + + private void handleTabOffset(byte cc2) { + // Formula for tab offset handling adapted from: + // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac + // We're ignoring any tab offsets that do not occur at the beginning of a new cue. + // This is not conform the spec, but works in most cases. + if (captionStringBuilder.length() == 0) { + switch (cc2) { + case CTRL_TAB_OFFSET_1: + tabOffset++; + break; + case CTRL_TAB_OFFSET_2: + tabOffset += 2; + break; + case CTRL_TAB_OFFSET_3: + tabOffset += 3; + break; + } + } + } + + private int getSpanStartIndex() { + return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0; + } + + /** + * Applies a Span to the SpannableStringBuilder. + */ + private void applySpan() { + // Check if we have to do anything. + if (captionStyles.size() == 0) { + return; + } + + for (Integer startIndex : captionStyles.keySet()) { + CharacterStyle captionStyle = captionStyles.get(startIndex); + captionStringBuilder.setSpan(captionStyle, startIndex, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + captionStyles.remove(startIndex); + } + } + + /** + * Builds a cue from whatever is in the SpannableStringBuilder now. + */ + private void buildCue() { + applySpan(); // Apply Spans + CharSequence captionString = getDisplayCaption(); + if (captionString != null) { + cueIndent = tabOffset * 2.5f + cueIndent; + tabOffset = 0; + Cue cue = new Cue(captionString, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, cueIndent / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + cues.add(cue); + if (captionMode == CC_MODE_POP_ON) { + captionStringBuilder.clear(); + captionStringBuilder.clearSpans(); + cueLine = DEFAULT_CUE_LINE; + } + cueIndent = DEFAULT_INDENT; + } + } + + private void flushCaptionBuilder() { + captionStringBuilder.clear(); + captionStringBuilder.clearSpans(); + } + private void backspace() { if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); + captionStringBuilder.replace(captionStringBuilder.length() - 1, captionStringBuilder.length(), ""); } } @@ -450,7 +702,7 @@ public final class Eia608Decoder implements SubtitleDecoder { } } - private String getDisplayCaption() { + private CharSequence getDisplayCaption() { int buildLength = captionStringBuilder.length(); if (buildLength == 0) { return null; @@ -463,19 +715,19 @@ public final class Eia608Decoder implements SubtitleDecoder { int endIndex = endsWithNewline ? buildLength - 1 : buildLength; if (captionMode != CC_MODE_ROLL_UP) { - return captionStringBuilder.substring(0, endIndex); + return captionStringBuilder.subSequence(0, endIndex); } int startIndex = 0; int searchBackwardFromIndex = endIndex; for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) { - searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1); + searchBackwardFromIndex = captionStringBuilder.toString().lastIndexOf("\n", searchBackwardFromIndex - 1); } if (searchBackwardFromIndex != -1) { startIndex = searchBackwardFromIndex + 1; } captionStringBuilder.delete(0, startIndex); - return captionStringBuilder.substring(0, endIndex - startIndex); + return captionStringBuilder.subSequence(0, endIndex - startIndex); } private void setCaptionMode(int captionMode) { @@ -485,10 +737,11 @@ public final class Eia608Decoder implements SubtitleDecoder { this.captionMode = captionMode; // Clear the working memory. - captionStringBuilder.setLength(0); + captionStringBuilder.clear(); + captionStringBuilder.clearSpans(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { // When switching to roll-up or unknown, we also need to clear the caption. - captionString = null; + cues = new LinkedList<>(); } } @@ -525,6 +778,11 @@ public final class Eia608Decoder implements SubtitleDecoder { return cc1 >= 0x10 && cc1 <= 0x1F; } + private static boolean isTabOffset(byte cc1, byte cc2) { + return (cc1 == CTRL_TAB_OFFSET_CHAN_1 || cc1 == CTRL_TAB_OFFSET_CHAN_2) + && (cc2 >= 0x21 && cc2 <= 0x23); + } + /** * Inspects an sei message to determine whether it contains EIA-608. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java index 6b27004174..ae0ecd4867 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java @@ -15,10 +15,9 @@ */ package com.google.android.exoplayer2.text.eia608; -import android.text.TextUtils; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; -import java.util.Collections; + import java.util.List; /** @@ -26,13 +25,10 @@ import java.util.List; */ /* package */ final class Eia608Subtitle implements Subtitle { - private final String text; + private final List cues; - /** - * @param text The subtitle text. - */ - public Eia608Subtitle(String text) { - this.text = text; + public Eia608Subtitle(List cues) { + this.cues = cues; } @Override @@ -52,11 +48,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - if (TextUtils.isEmpty(text)) { - return Collections.emptyList(); - } else { - return Collections.singletonList(new Cue(text)); - } + return cues; } } From 8b2af1244725b8d600c3f5ea2cceb4504728c7d9 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 14:58:08 +0200 Subject: [PATCH 002/206] Discard spans after seeking Discard any spans that should not be applied because they don't belong to what's in the current CC buffer. --- .../android/exoplayer2/text/eia608/Eia608Decoder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index c54cc2ef5e..ecd2c766c4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -656,9 +656,13 @@ public final class Eia608Decoder implements SubtitleDecoder { } for (Integer startIndex : captionStyles.keySet()) { - CharacterStyle captionStyle = captionStyles.get(startIndex); - captionStringBuilder.setSpan(captionStyle, startIndex, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // There may be cases, e.g. when seeking where the startIndex becomes greater + // than what is actually in the string builder, in that case, just discard the span. + if (startIndex < captionStringBuilder.length()) { + CharacterStyle captionStyle = captionStyles.get(startIndex); + captionStringBuilder.setSpan(captionStyle, startIndex, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } captionStyles.remove(startIndex); } } From ce55587db48ae0f7be24a4817af26a444f69cc8d Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 16:27:23 +0200 Subject: [PATCH 003/206] Fixed comment Alpha is stored in the first byte of an integer --- .../google/android/exoplayer2/text/eia608/Eia608Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index ecd2c766c4..f64cccaa4f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -224,7 +224,7 @@ public final class Eia608Decoder implements SubtitleDecoder { Color.BLACK // Only used by Mid Row style changes, for PAC an value of 0x7 means italics. }; - // Transparency is defined in the two left most bytes of an integer. + // Transparency is defined in the first byte of an integer. private static final int TRANSPARENCY_MASK = 0x80FFFFFF; private static final int STYLE_ITALIC = Typeface.ITALIC; From 176ce485696cfda8418e7426dc6a7e25999096f5 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 25 Aug 2016 10:07:05 +0200 Subject: [PATCH 004/206] Improved line and tab offset mapping --- .../exoplayer2/text/eia608/Eia608Decoder.java | 82 +++---------------- 1 file changed, 10 insertions(+), 72 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index f64cccaa4f..0d82385fc8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -101,9 +101,6 @@ public final class Eia608Decoder implements SubtitleDecoder { private static final byte CTRL_TAB_OFFSET_CHAN_1 = 0x17; private static final byte CTRL_TAB_OFFSET_CHAN_2 = 0x1F; - private static final byte CTRL_TAB_OFFSET_1 = 0x21; - private static final byte CTRL_TAB_OFFSET_2 = 0x22; - private static final byte CTRL_TAB_OFFSET_3 = 0x23; private static final byte CTRL_BACKSPACE = 0x21; @@ -181,21 +178,14 @@ public final class Eia608Decoder implements SubtitleDecoder { // Maps EIA-608 PAC row numbers to WebVTT cue line settings. // Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac private static final float[] CUE_LINE_MAP = new float[] { + 63.33f, // Row 11 10.00f, // Row 1 - 15.33f, - 20.66f, - 26.00f, - 31.33f, - 36.66f, - 42.00f, - 47.33f, - 52.66f, - 58.00f, - 63.33f, - 68.66f, - 74.00f, - 79.33f, - 84.66f // Row 15 + 26.00f, // Row 4 + 68.66f, // Row 12 + 79.33f, // Row 14 + 31.33f, // Row 5 + 42.00f, // Row 7 + 52.66f, // Row 9 }; // Maps EIA-608 PAC indents to WebVTT cue position values. @@ -228,7 +218,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private static final int TRANSPARENCY_MASK = 0x80FFFFFF; private static final int STYLE_ITALIC = Typeface.ITALIC; - private static final float DEFAULT_CUE_LINE = CUE_LINE_MAP[10]; // Row 11 + private static final float DEFAULT_CUE_LINE = CUE_LINE_MAP[4]; // Row 11 private static final float DEFAULT_INDENT = INDENT_MAP[0]; // Indent 0 private final LinkedList availableInputBuffers; @@ -560,49 +550,7 @@ public final class Eia608Decoder implements SubtitleDecoder { } // Parse the row bits - int row = cc1 & 0x7; - if (row >= 0x4) { - // Extended Preamble Code - row = row & 0x3; - switch (row) { - case 0x0: - // Row 14 or 15 - cueLine = CUE_LINE_MAP[13]; - break; - case 0x1: - // Row 5 or 6 - cueLine = CUE_LINE_MAP[4]; - break; - case 0x2: - // Row 7 or 8 - cueLine = CUE_LINE_MAP[7]; - break; - case 0x3: - // Row 9 or 10 - cueLine = CUE_LINE_MAP[8]; - break; - } - } else { - // Regular Preamble Code - switch (row) { - case 0x0: - // Row 11 (Default) - cueLine = CUE_LINE_MAP[10]; - break; - case 0x1: - // Row 1 (Top) - cueLine = CUE_LINE_MAP[0]; - break; - case 0x2: - // Row 4 (Top) - cueLine = CUE_LINE_MAP[3]; - break; - case 0x3: - // Row 12 or 13 (Bottom) - cueLine = CUE_LINE_MAP[11]; - break; - } - } + cueLine = CUE_LINE_MAP[cc1 & 0x7]; } private void handleMidrowCode(byte cc1, byte cc2) { @@ -628,17 +576,7 @@ public final class Eia608Decoder implements SubtitleDecoder { // We're ignoring any tab offsets that do not occur at the beginning of a new cue. // This is not conform the spec, but works in most cases. if (captionStringBuilder.length() == 0) { - switch (cc2) { - case CTRL_TAB_OFFSET_1: - tabOffset++; - break; - case CTRL_TAB_OFFSET_2: - tabOffset += 2; - break; - case CTRL_TAB_OFFSET_3: - tabOffset += 3; - break; - } + tabOffset = cc2 - 0x20; } } From 7cd94819c7a4a0222022cbfd49a1d77f30aa5812 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 25 Aug 2016 11:23:03 +0200 Subject: [PATCH 005/206] Took care of caption styling priorities --- .../exoplayer2/text/eia608/Eia608Decoder.java | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 0d82385fc8..6b76973278 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -513,7 +513,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private void handlePreambleCode(byte cc1, byte cc2) { // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands - applySpan(); // Apply any spans. + applySpans(); // Apply any open spans. // Parse the "next row down" flag. boolean nextRowDown = (cc2 & 0x20) != 0; @@ -528,17 +528,17 @@ public final class Eia608Decoder implements SubtitleDecoder { // Go through the bits, starting with the last bit - the underline flag: boolean underline = (cc2 & 0x1) != 0; if (underline) { - captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + setCharacterStyle(new UnderlineSpan()); } // Next, parse the attribute bits: int attribute = cc2 >> 1 & 0xF; if (attribute >= 0x0 && attribute < 0x7) { // Attribute is a foreground color - captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute])); + setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); } else if (attribute == 0x7) { // Attribute is "italics" - captionStyles.put(getSpanStartIndex(), new StyleSpan(STYLE_ITALIC)); + setCharacterStyle(new StyleSpan(STYLE_ITALIC)); } else if (attribute >= 0x8 && attribute <= 0xF) { // Attribute is an indent if (cueIndent == DEFAULT_INDENT) { @@ -558,14 +558,14 @@ public final class Eia608Decoder implements SubtitleDecoder { int attribute = cc2 >> 1 & 0xF; if ((cc1 & 0x1) != 0) { // Background Color - captionStyles.put(getSpanStartIndex(), new BackgroundColorSpan(transparentOrUnderline ? + setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); } else { // Foreground color - captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute])); + setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); if (transparentOrUnderline) { // Text should be underlined - captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + setCharacterStyle(new UnderlineSpan()); } } } @@ -585,9 +585,43 @@ public final class Eia608Decoder implements SubtitleDecoder { } /** - * Applies a Span to the SpannableStringBuilder. + * Sets a character style at the current cueIndex. + * Takes care of style priorities. + * + * @param style the style to set. */ - private void applySpan() { + private void setCharacterStyle(CharacterStyle style) { + int startIndex = getSpanStartIndex(); + // Close all open spans of the same type, and add a new one. + if (style instanceof ForegroundColorSpan) { + // Setting a foreground color clears the italics style. + applySpan(StyleSpan.class); // + } + applySpan(style.getClass()); + captionStyles.put(startIndex, style); + } + + /** + * Closes all open spans of the spansToApply class. + * @param spansToClose the class of which the spans should be closed. + */ + private void applySpan(Class spansToClose) { + for (Integer index : captionStyles.keySet()) { + CharacterStyle style = captionStyles.get(index); + if (spansToClose.isInstance(style)) { + if (index < captionStringBuilder.length()) { + captionStringBuilder.setSpan(style, index, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + captionStyles.remove(index); + } + } + } + + /** + * Applies all currently opened spans to the SpannableStringBuilder. + */ + private void applySpans() { // Check if we have to do anything. if (captionStyles.size() == 0) { return; @@ -609,7 +643,7 @@ public final class Eia608Decoder implements SubtitleDecoder { * Builds a cue from whatever is in the SpannableStringBuilder now. */ private void buildCue() { - applySpan(); // Apply Spans + applySpans(); // Apply Spans CharSequence captionString = getDisplayCaption(); if (captionString != null) { cueIndent = tabOffset * 2.5f + cueIndent; @@ -633,7 +667,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private void backspace() { if (captionStringBuilder.length() > 0) { - captionStringBuilder.replace(captionStringBuilder.length() - 1, captionStringBuilder.length(), ""); + captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length()); } } From 71d83f3e8478f05271845eb36a0a3f1bfec98808 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Mon, 29 Aug 2016 11:33:55 +0200 Subject: [PATCH 006/206] Caption lines as separate cues --- .../text/eia608/Eia608CueBuilder.java | 203 +++++++++++ .../exoplayer2/text/eia608/Eia608Decoder.java | 322 +++++------------- .../text/eia608/Eia608Subtitle.java | 9 +- .../exoplayer2/ui/SubtitlePainter.java | 7 +- 4 files changed, 298 insertions(+), 243 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java new file mode 100644 index 0000000000..336ea6dc27 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -0,0 +1,203 @@ +package com.google.android.exoplayer2.text.eia608; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; + +import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * A Builder for EIA-608 cues. + */ +/* package */ final class Eia608CueBuilder { + + private static final int BASE_ROW = 15; + + /** + * The caption string. + */ + private SpannableStringBuilder captionStringBuilder; + + /** + * The caption styles to apply to the caption string. + */ + private HashMap captionStyles; + + /** + * The row on which the Cue should be displayed. + */ + private int row; + + /** + * The indent of the cue - horizontal positioning. + */ + private int indent; + + /** + * The setTabOffset offset for the cue. + */ + private int tabOffset; + + public Eia608CueBuilder() { + row = BASE_ROW; + indent = 0; + tabOffset = 0; + captionStringBuilder = new SpannableStringBuilder(); + captionStyles = new HashMap<>(); + } + + /** + * Sets the row for this cue. + * @param row the row to set. + */ + public void setRow(int row) { + Assertions.checkArgument(row >= 1 && row <= 15); + this.row = row; + } + + public int getRow() { + return row; + } + + /** + * Rolls up the Cue one row. + * @return true if rolling was possible. + */ + public boolean rollUp() { + if (row < 1) { + return false; + } + setRow(row - 1); + return true; + } + + /** + * Sets the indent for this cue. + * @param indent an indent value, must be a multiple of 4 within the range [0,28] + */ + public void setIndent(int indent) { + Assertions.checkArgument(indent % 4 == 0 && indent <= 28); + this.indent = indent; + } + + public void tab(int tabs) { + tabOffset += tabs; + } + + /** + * Indents the cue position with amountOfTabs. + * @param tabOffset the amount of tabs the cue position should be indented. + */ + public void setTabOffset(int tabOffset) { + this.tabOffset = tabOffset; + } + + /** + * Appends a character to the current Cue. + * @param character the character to append. + */ + public void append(char character) { + captionStringBuilder.append(character); + } + + /** + * Removes the last character of the caption string. + */ + public void backspace() { + if (captionStringBuilder.length() > 0) { + captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length()); + } + } + + /** + * Opens a character style at the current cueIndex. + * Takes care of style priorities. + * + * @param style the style to set. + */ + public void setCharacterStyle(CharacterStyle style) { + int startIndex = getSpanStartIndex(); + // Close all open spans of the same type, and add a new one. + if (style instanceof ForegroundColorSpan) { + // Setting a foreground color clears the italics style. + closeSpan(StyleSpan.class); // Italics is a style span. + } + closeSpan(style.getClass()); + captionStyles.put(startIndex, style); + } + + /** + * Closes all open spans of the spansToApply class. + * @param spansToClose the class of which the spans should be closed. + */ + private void closeSpan(Class spansToClose) { + for (Integer index : captionStyles.keySet()) { + CharacterStyle style = captionStyles.get(index); + if (spansToClose.isInstance(style)) { + if (index < captionStringBuilder.length()) { + captionStringBuilder.setSpan(style, index, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + captionStyles.remove(index); + } + } + } + + /** + * Applies all currently opened spans to the SpannableStringBuilder. + */ + public void closeSpans() { + // Check if we have to do anything. + if (captionStyles.size() == 0) { + return; + } + + for (Integer startIndex : captionStyles.keySet()) { + // There may be cases, e.g. when seeking where the startIndex becomes greater + // than what is actually in the string builder, in that case, just discard the span. + if (startIndex < captionStringBuilder.length()) { + CharacterStyle captionStyle = captionStyles.get(startIndex); + captionStringBuilder.setSpan(captionStyle, startIndex, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + captionStyles.remove(startIndex); + } + } + + public Cue build() { + closeSpans(); + float cueLine = 10 + (5.33f * row); + float cuePosition = 10 + (2.5f * indent); + cuePosition = (tabOffset * 2.5f) + cuePosition; + return new Cue(captionStringBuilder, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, cuePosition / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + } + + private int getSpanStartIndex() { + return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0; + } + + public static List buildCues(List builders) { + if (builders.isEmpty()) { + return Collections.emptyList(); + } + LinkedList cues = new LinkedList<>(); + for (Eia608CueBuilder builder : builders) { + if (builder.captionStringBuilder.length() == 0) { + continue; + } + cues.add(builder.build()); + } + return cues; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 6b76973278..a22369ca76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -26,16 +26,12 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import android.graphics.Color; import android.graphics.Typeface; -import android.text.Layout; -import android.text.SpannableStringBuilder; -import android.text.Spanned; import android.text.style.BackgroundColorSpan; -import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; -import java.util.HashMap; +import java.util.Collections; import java.util.LinkedList; import java.util.TreeSet; @@ -98,6 +94,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; + private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; private static final byte CTRL_TAB_OFFSET_CHAN_1 = 0x17; private static final byte CTRL_TAB_OFFSET_CHAN_2 = 0x1F; @@ -175,33 +172,13 @@ public final class Eia608Decoder implements SubtitleDecoder { 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 }; - // Maps EIA-608 PAC row numbers to WebVTT cue line settings. - // Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac - private static final float[] CUE_LINE_MAP = new float[] { - 63.33f, // Row 11 - 10.00f, // Row 1 - 26.00f, // Row 4 - 68.66f, // Row 12 - 79.33f, // Row 14 - 31.33f, // Row 5 - 42.00f, // Row 7 - 52.66f, // Row 9 - }; + // Maps EIA-608 PAC row bits to rows. + private static final int[] ROW_INDICES = new int[] { 11, 1, 3, 12, 14, 5, 7, 9 }; - // Maps EIA-608 PAC indents to WebVTT cue position values. - // Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac - // Note that these cue position values may not give the intended result, unless the font size is set + // Maps EIA-608 PAC cursor bits to indents. + // Note that these indents may not give the intended result, unless the font size is set // to allow for a maximum of 32 (or 41) characters per line. - private static final float[] INDENT_MAP = new float[] { - 10.0f, // Indent 0/Column 1 - 20.0f, // Indent 4/Column 5 - 30.0f, // Indent 8/Column 9 - 40.0f, // Indent 12/Column 13 - 50.0f, // Indent 16/Column 17 - 60.0f, // Indent 20/Column 21 - 70.0f, // Indent 24/Column 25 - 80.0f, // Indent 28/Column 29 - }; + private static final int[] CURSOR_INDICES = new int[] { 0, 4, 8, 12, 16, 20, 24, 28 }; private static final int[] COLOR_MAP = new int[] { Color.WHITE, @@ -216,18 +193,14 @@ public final class Eia608Decoder implements SubtitleDecoder { // Transparency is defined in the first byte of an integer. private static final int TRANSPARENCY_MASK = 0x80FFFFFF; - private static final int STYLE_ITALIC = Typeface.ITALIC; - private static final float DEFAULT_CUE_LINE = CUE_LINE_MAP[4]; // Row 11 - private static final float DEFAULT_INDENT = INDENT_MAP[0]; // Indent 0 private final LinkedList availableInputBuffers; private final LinkedList availableOutputBuffers; private final TreeSet queuedInputBuffers; private final ParsableByteArray ccData; - - private final SpannableStringBuilder captionStringBuilder; + private final LinkedList cues; private long playbackPositionUs; @@ -235,17 +208,17 @@ public final class Eia608Decoder implements SubtitleDecoder { private int captionMode; private int captionRowCount; + boolean nextRowDown; - private LinkedList cues; - private HashMap captionStyles; - float cueIndent; - float cueLine; - int tabOffset; + // The Cue that's currently being built and decoded. + private Eia608CueBuilder currentCue; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; + private Eia608Subtitle subtitle; + public Eia608Decoder() { availableInputBuffers = new LinkedList<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { @@ -258,15 +231,12 @@ public final class Eia608Decoder implements SubtitleDecoder { queuedInputBuffers = new TreeSet<>(); ccData = new ParsableByteArray(); - - captionStringBuilder = new SpannableStringBuilder(); - captionStyles = new HashMap<>(); + subtitle = new Eia608Subtitle(); + cues = new LinkedList<>(); + nextRowDown = false; setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; - cueIndent = DEFAULT_INDENT; - cueLine = DEFAULT_CUE_LINE; - tabOffset = 0; } @Override @@ -322,14 +292,11 @@ public final class Eia608Decoder implements SubtitleDecoder { decode(inputBuffer); // check if we have any caption updates to report - if (!cues.isEmpty()) { - if (!inputBuffer.isDecodeOnly()) { - SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(cues), 0); - cues = new LinkedList<>(); - releaseInputBuffer(inputBuffer); - return outputBuffer; - } + if (!inputBuffer.isDecodeOnly()) { + SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); + outputBuffer.setContent(inputBuffer.timeUs, subtitle, 0); + releaseInputBuffer(inputBuffer); + return outputBuffer; } releaseInputBuffer(inputBuffer); @@ -353,11 +320,9 @@ public final class Eia608Decoder implements SubtitleDecoder { setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; playbackPositionUs = 0; - flushCaptionBuilder(); - cues = new LinkedList<>(); - cueIndent = DEFAULT_INDENT; - cueLine = DEFAULT_CUE_LINE; - tabOffset = 0; + currentCue = new Eia608CueBuilder(); + cues.clear(); + nextRowDown = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -393,23 +358,23 @@ public final class Eia608Decoder implements SubtitleDecoder { // Special North American character set. // ccData2 - P|0|1|1|X|X|X|X if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) { - captionStringBuilder.append(getSpecialChar(ccData2)); + currentCue.append(getSpecialChar(ccData2)); continue; } // Extended Spanish/Miscellaneous and French character set. // ccData2 - P|0|1|X|X|X|X|X if ((ccData1 == 0x12 || ccData1 == 0x1A) && ((ccData2 & 0x60) == 0x20)) { - backspace(); // Remove standard equivalent of the special extended char. - captionStringBuilder.append(getExtendedEsFrChar(ccData2)); + currentCue.backspace(); // Remove standard equivalent of the special extended char. + currentCue.append(getExtendedEsFrChar(ccData2)); continue; } // Extended Portuguese and German/Danish character set. // ccData2 - P|0|1|X|X|X|X|X if ((ccData1 == 0x13 || ccData1 == 0x1B) && ((ccData2 & 0x60) == 0x20)) { - backspace(); // Remove standard equivalent of the special extended char. - captionStringBuilder.append(getExtendedPtDeChar(ccData2)); + currentCue.backspace(); // Remove standard equivalent of the special extended char. + currentCue.append(getExtendedPtDeChar(ccData2)); continue; } @@ -425,9 +390,9 @@ public final class Eia608Decoder implements SubtitleDecoder { } // Basic North American character set. - captionStringBuilder.append(getChar(ccData1)); + currentCue.append(getChar(ccData1)); if (ccData2 >= 0x20) { - captionStringBuilder.append(getChar(ccData2)); + currentCue.append(getChar(ccData2)); } } @@ -435,8 +400,8 @@ public final class Eia608Decoder implements SubtitleDecoder { if (!isRepeatableControl) { repeatableControlSet = false; } - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - buildCue(); + if (captionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP) { + renderCues(); } } } @@ -456,7 +421,7 @@ public final class Eia608Decoder implements SubtitleDecoder { if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { - handlePreambleCode(cc1, cc2); + handlePreambleAddressCode(cc1, cc2); } else if (isTabOffset(cc1, cc2)) { handleTabOffset(cc2); } @@ -492,65 +457,75 @@ public final class Eia608Decoder implements SubtitleDecoder { switch (cc2) { case CTRL_ERASE_DISPLAYED_MEMORY: if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - flushCaptionBuilder(); + currentCue = new Eia608CueBuilder(); + cues.clear(); } + subtitle.setCues(Collections.emptyList()); return; case CTRL_ERASE_NON_DISPLAYED_MEMORY: - flushCaptionBuilder(); + currentCue = new Eia608CueBuilder(); + cues.clear(); return; case CTRL_END_OF_CAPTION: - buildCue(); - flushCaptionBuilder(); + renderCues(); + cues.clear(); return; case CTRL_CARRIAGE_RETURN: - maybeAppendNewline(); + // Each time a Carriage Return is received, the text in the top row of the window is erased + // from memory and from the display. The remaining rows of text are each rolled up into the + // next highest row in the window, leaving the base row blank and ready to accept new text. + if (captionMode == CC_MODE_ROLL_UP) { + for (Eia608CueBuilder cue : cues) { + // Roll up all the other rows. + if (!cue.rollUp()) { + cues.remove(cue); + } + } + currentCue = new Eia608CueBuilder(); + cues.add(currentCue); + while (cues.size() > captionRowCount) { + cues.pollFirst(); + } + } return; case CTRL_BACKSPACE: - backspace(); + currentCue.backspace(); + return; + case CTRL_DELETE_TO_END_OF_ROW: + // TODO: Clear currentCue's captionText. return; } } - private void handlePreambleCode(byte cc1, byte cc2) { + private void handlePreambleAddressCode(byte cc1, byte cc2) { // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands - applySpans(); // Apply any open spans. - // Parse the "next row down" flag. - boolean nextRowDown = (cc2 & 0x20) != 0; - if (nextRowDown) { - // TODO: We should create a new cue instead, this may cause issues when - // the new line receives it's own PAC which we ignore currently. - // As a result of that the new line will be positioned directly below the - // previous line. - maybeAppendNewline(); + // Parse the "next row down" toggle. + nextRowDown = (cc2 & 0x20) != 0; + + int row = ROW_INDICES[cc1 & 0x7]; + if (row != currentCue.getRow() || nextRowDown) { + currentCue = new Eia608CueBuilder(); + cues.add(currentCue); } + currentCue.setRow(nextRowDown ? ++row : row); - // Go through the bits, starting with the last bit - the underline flag: boolean underline = (cc2 & 0x1) != 0; if (underline) { - setCharacterStyle(new UnderlineSpan()); + currentCue.setCharacterStyle(new UnderlineSpan()); } - // Next, parse the attribute bits: int attribute = cc2 >> 1 & 0xF; if (attribute >= 0x0 && attribute < 0x7) { // Attribute is a foreground color - setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); } else if (attribute == 0x7) { // Attribute is "italics" - setCharacterStyle(new StyleSpan(STYLE_ITALIC)); + currentCue.setCharacterStyle(new StyleSpan(STYLE_ITALIC)); } else if (attribute >= 0x8 && attribute <= 0xF) { // Attribute is an indent - if (cueIndent == DEFAULT_INDENT) { - // Only update the indent, if it's the default indent. - // This is not conform the spec, but otherwise indentations may be off - // because we don't create a new cue when we see the nextRowDown flag. - cueIndent = INDENT_MAP[attribute & 0x7]; - } + currentCue.setIndent(CURSOR_INDICES[attribute & 0x7]); } - - // Parse the row bits - cueLine = CUE_LINE_MAP[cc1 & 0x7]; } private void handleMidrowCode(byte cc1, byte cc2) { @@ -558,152 +533,20 @@ public final class Eia608Decoder implements SubtitleDecoder { int attribute = cc2 >> 1 & 0xF; if ((cc1 & 0x1) != 0) { // Background Color - setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? + currentCue.setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); } else { // Foreground color - setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); if (transparentOrUnderline) { // Text should be underlined - setCharacterStyle(new UnderlineSpan()); + currentCue.setCharacterStyle(new UnderlineSpan()); } } } private void handleTabOffset(byte cc2) { - // Formula for tab offset handling adapted from: - // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac - // We're ignoring any tab offsets that do not occur at the beginning of a new cue. - // This is not conform the spec, but works in most cases. - if (captionStringBuilder.length() == 0) { - tabOffset = cc2 - 0x20; - } - } - - private int getSpanStartIndex() { - return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0; - } - - /** - * Sets a character style at the current cueIndex. - * Takes care of style priorities. - * - * @param style the style to set. - */ - private void setCharacterStyle(CharacterStyle style) { - int startIndex = getSpanStartIndex(); - // Close all open spans of the same type, and add a new one. - if (style instanceof ForegroundColorSpan) { - // Setting a foreground color clears the italics style. - applySpan(StyleSpan.class); // - } - applySpan(style.getClass()); - captionStyles.put(startIndex, style); - } - - /** - * Closes all open spans of the spansToApply class. - * @param spansToClose the class of which the spans should be closed. - */ - private void applySpan(Class spansToClose) { - for (Integer index : captionStyles.keySet()) { - CharacterStyle style = captionStyles.get(index); - if (spansToClose.isInstance(style)) { - if (index < captionStringBuilder.length()) { - captionStringBuilder.setSpan(style, index, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - captionStyles.remove(index); - } - } - } - - /** - * Applies all currently opened spans to the SpannableStringBuilder. - */ - private void applySpans() { - // Check if we have to do anything. - if (captionStyles.size() == 0) { - return; - } - - for (Integer startIndex : captionStyles.keySet()) { - // There may be cases, e.g. when seeking where the startIndex becomes greater - // than what is actually in the string builder, in that case, just discard the span. - if (startIndex < captionStringBuilder.length()) { - CharacterStyle captionStyle = captionStyles.get(startIndex); - captionStringBuilder.setSpan(captionStyle, startIndex, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - captionStyles.remove(startIndex); - } - } - - /** - * Builds a cue from whatever is in the SpannableStringBuilder now. - */ - private void buildCue() { - applySpans(); // Apply Spans - CharSequence captionString = getDisplayCaption(); - if (captionString != null) { - cueIndent = tabOffset * 2.5f + cueIndent; - tabOffset = 0; - Cue cue = new Cue(captionString, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, cueIndent / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); - cues.add(cue); - if (captionMode == CC_MODE_POP_ON) { - captionStringBuilder.clear(); - captionStringBuilder.clearSpans(); - cueLine = DEFAULT_CUE_LINE; - } - cueIndent = DEFAULT_INDENT; - } - } - - private void flushCaptionBuilder() { - captionStringBuilder.clear(); - captionStringBuilder.clearSpans(); - } - - private void backspace() { - if (captionStringBuilder.length() > 0) { - captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length()); - } - } - - private void maybeAppendNewline() { - int buildLength = captionStringBuilder.length(); - if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') { - captionStringBuilder.append('\n'); - } - } - - private CharSequence getDisplayCaption() { - int buildLength = captionStringBuilder.length(); - if (buildLength == 0) { - return null; - } - - boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n'; - if (buildLength == 1 && endsWithNewline) { - return null; - } - - int endIndex = endsWithNewline ? buildLength - 1 : buildLength; - if (captionMode != CC_MODE_ROLL_UP) { - return captionStringBuilder.subSequence(0, endIndex); - } - - int startIndex = 0; - int searchBackwardFromIndex = endIndex; - for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) { - searchBackwardFromIndex = captionStringBuilder.toString().lastIndexOf("\n", searchBackwardFromIndex - 1); - } - if (searchBackwardFromIndex != -1) { - startIndex = searchBackwardFromIndex + 1; - } - captionStringBuilder.delete(0, startIndex); - return captionStringBuilder.subSequence(0, endIndex - startIndex); + currentCue.tab(cc2 - 0x20); } private void setCaptionMode(int captionMode) { @@ -713,14 +556,17 @@ public final class Eia608Decoder implements SubtitleDecoder { this.captionMode = captionMode; // Clear the working memory. - captionStringBuilder.clear(); - captionStringBuilder.clearSpans(); + currentCue = new Eia608CueBuilder(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { // When switching to roll-up or unknown, we also need to clear the caption. - cues = new LinkedList<>(); + cues.clear(); } } + private void renderCues() { + subtitle.setCues(Eia608CueBuilder.buildCues(cues)); + } + private static char getChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java index ae0ecd4867..674bee9488 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.eia608; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; +import java.util.LinkedList; import java.util.List; /** @@ -25,9 +26,13 @@ import java.util.List; */ /* package */ final class Eia608Subtitle implements Subtitle { - private final List cues; + private List cues; - public Eia608Subtitle(List cues) { + public Eia608Subtitle() { + cues = new LinkedList<>(); + } + + /* package */ void setCues(List cues) { this.cues = cues; } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index cb4eec40f1..4ebfc11237 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.ui; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Util; + import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -30,9 +34,6 @@ import android.text.TextPaint; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.util.Util; /** * Paints subtitle {@link Cue}s. From 35fa5e2ef7d230f5704f562ec5a48d096ec9385f Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 10:57:04 +0200 Subject: [PATCH 007/206] Fixed foreground color midrow codes being handled as background colors --- .../exoplayer2/text/eia608/Eia608Decoder.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index a22369ca76..16062ec6eb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -379,7 +379,7 @@ public final class Eia608Decoder implements SubtitleDecoder { } // Mid row changes. - if ((ccData1 == 0x11 || ccData1 == 0x19) && ccData2 >= 0x20 && ccData2 <= 0x2F) { + if ((ccData1 == 0x11 || ccData1 == 0x19) && (ccData2 >= 0x20 && ccData2 <= 0x2F)) { handleMidrowCode(ccData1, ccData2); } @@ -531,13 +531,18 @@ public final class Eia608Decoder implements SubtitleDecoder { private void handleMidrowCode(byte cc1, byte cc2) { boolean transparentOrUnderline = (cc2 & 0x1) != 0; int attribute = cc2 >> 1 & 0xF; - if ((cc1 & 0x1) != 0) { + if ((cc1 & 0x1) == 0) { // Background Color currentCue.setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); } else { - // Foreground color - currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + if (attribute < 7) { + // Foreground color + currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + } else { + // Italics + currentCue.setCharacterStyle(new StyleSpan(STYLE_ITALIC)); + } if (transparentOrUnderline) { // Text should be underlined currentCue.setCharacterStyle(new UnderlineSpan()); From ad9f76eb62d9f2ee99466de965fc2c6a7eb4f746 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 11:13:32 +0200 Subject: [PATCH 008/206] Return a copy of the SpannableStringBuilder when building Cues In order to force the SubtitlePainter to redraw cues --- .../android/exoplayer2/text/eia608/Eia608CueBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index 336ea6dc27..eb940ec236 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -178,7 +178,8 @@ import java.util.List; float cueLine = 10 + (5.33f * row); float cuePosition = 10 + (2.5f * indent); cuePosition = (tabOffset * 2.5f) + cuePosition; - return new Cue(captionStringBuilder, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, + return new Cue(new SpannableStringBuilder(captionStringBuilder), + Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START, cuePosition / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); } From 4d18e623d602c54e2e4d60518627f926ce5402f0 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 11:55:41 +0200 Subject: [PATCH 009/206] Split Cue Rendering up into 3 phases Split Cue Rendering up into a layout, background drawing and foreground drawing phase so that issues with overlapping 608 captions are being prevented. --- .../text/eia608/Eia608CueBuilder.java | 12 ++--- .../exoplayer2/ui/SubtitlePainter.java | 45 ++++++++++++------- .../android/exoplayer2/ui/SubtitleView.java | 20 ++++++--- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index eb940ec236..3531b4dcc2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -89,18 +89,14 @@ import java.util.List; this.indent = indent; } + /** + * Indents the Cue. + * @param tabs The amount of tabs to indent the cue with. + */ public void tab(int tabs) { tabOffset += tabs; } - /** - * Indents the cue position with amountOfTabs. - * @param tabOffset the amount of tabs the cue position should be indented. - */ - public void setTabOffset(int tabOffset) { - this.tabOffset = tabOffset; - } - /** * Appends a character to the current Cue. * @param character the character to append. diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 4ebfc11237..3930de584f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -117,30 +117,30 @@ import android.util.Log; } /** - * Draws the provided {@link Cue} into a canvas with the specified styling. + * Creates layouts so that the provided {@link Cue} can be drawn in a {@link Canvas} by calls to + * {@link #drawBackground(Canvas)} and {@link #drawForeground(Canvas)} with the specified styling. *

* A call to this method is able to use cached results of calculations made during the previous * call, and so an instance of this class is able to optimize repeated calls to this method in * which the same parameters are passed. * - * @param cue The cue to draw. + * @param cue The cue to layout. * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied. * @param style The style to use when drawing the cue text. * @param textSizePx The text size to use when drawing the cue text, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height - * @param canvas The canvas into which to draw. * @param cueBoxLeft The left position of the enclosing cue box. * @param cueBoxTop The top position of the enclosing cue box. * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, - float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, - int cueBoxBottom) { + public void layout(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, + float bottomPaddingFraction, int cueBoxLeft, int cueBoxTop, int cueBoxRight, + int cueBoxBottom) { CharSequence cueText = cue.text; if (TextUtils.isEmpty(cueText)) { - // Nothing to draw. + // Nothing to layout. return; } if (!applyEmbeddedStyles) { @@ -169,7 +169,6 @@ import android.util.Log; && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. - drawLayout(canvas); return; } @@ -272,19 +271,17 @@ import android.util.Log; this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; - - drawLayout(canvas); } /** - * Draws {@link #textLayout} into the provided canvas. + * Draws the background of the {@link #textLayout} into the provided canvas. * - * @param canvas The canvas into which to draw. + * @param canvas The canvas into which to layout. */ - private void drawLayout(Canvas canvas) { + public void drawBackground(Canvas canvas) { final StaticLayout layout = textLayout; if (layout == null) { - // Nothing to draw. + // Nothing to layout. return; } @@ -311,6 +308,24 @@ import android.util.Log; } } + canvas.restoreToCount(saveCount); + } + + /** + * Draws the foreground of the {@link #textLayout} into the provided canvas. + * + * @param canvas The canvas into which to layout. + */ + public void drawForeground(Canvas canvas) { + final StaticLayout layout = textLayout; + if (layout == null) { + // Nothing to layout. + return; + } + + int saveCount = canvas.save(); + canvas.translate(textLeft, textTop); + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeWidth(outlineWidth); @@ -320,7 +335,7 @@ import android.util.Log; } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED - || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { + || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; int colorUp = raised ? Color.WHITE : edgeColor; int colorDown = raised ? edgeColor : Color.WHITE; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 0c8d9ef92e..70a0787352 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -15,6 +15,11 @@ */ package com.google.android.exoplayer2.ui; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.util.Util; + import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; @@ -23,10 +28,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.accessibility.CaptioningManager; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextRenderer; -import com.google.android.exoplayer2.util.Util; + import java.util.ArrayList; import java.util.List; @@ -230,7 +232,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { int right = getRight() + getPaddingRight(); int bottom = rawBottom - getPaddingBottom(); if (bottom <= top || right <= left) { - // No space to draw subtitles. + // No space to layout subtitles. return; } @@ -242,8 +244,12 @@ public final class SubtitleView extends View implements TextRenderer.Output { } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, style, textSizePx, - bottomPaddingFraction, canvas, left, top, right, bottom); + painters.get(i).layout(cues.get(i), applyEmbeddedStyles, style, textSizePx, + bottomPaddingFraction, left, top, right, bottom); + painters.get(i).drawBackground(canvas); + } + for (int i = 0; i < cueCount; i++) { + painters.get(i).drawForeground(canvas); } } From 26fe1dc2384902ba3f3b5c36afca03162fc0dc0d Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 1 Sep 2016 15:01:17 +0200 Subject: [PATCH 010/206] Removed unnecessary member variable --- .../google/android/exoplayer2/text/eia608/Eia608Decoder.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 16062ec6eb..48fa816977 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -208,7 +208,6 @@ public final class Eia608Decoder implements SubtitleDecoder { private int captionMode; private int captionRowCount; - boolean nextRowDown; // The Cue that's currently being built and decoded. private Eia608CueBuilder currentCue; @@ -233,7 +232,6 @@ public final class Eia608Decoder implements SubtitleDecoder { ccData = new ParsableByteArray(); subtitle = new Eia608Subtitle(); cues = new LinkedList<>(); - nextRowDown = false; setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; @@ -322,7 +320,6 @@ public final class Eia608Decoder implements SubtitleDecoder { playbackPositionUs = 0; currentCue = new Eia608CueBuilder(); cues.clear(); - nextRowDown = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -501,7 +498,7 @@ public final class Eia608Decoder implements SubtitleDecoder { // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands // Parse the "next row down" toggle. - nextRowDown = (cc2 & 0x20) != 0; + boolean nextRowDown = (cc2 & 0x20) != 0; int row = ROW_INDICES[cc1 & 0x7]; if (row != currentCue.getRow() || nextRowDown) { From 18ab96349eda2030ec295e92d5ffbe8ab3104e61 Mon Sep 17 00:00:00 2001 From: Alan Snyder Date: Fri, 2 Sep 2016 20:11:26 -0700 Subject: [PATCH 011/206] Support ID3/Apple metadata parsing in MP3 and MP4 files --- .../android/exoplayer2/demo/EventLogger.java | 19 +- .../metadata/id3/Id3DecoderTest.java | 10 +- .../com/google/android/exoplayer2/Format.java | 55 ++- .../android/exoplayer2/SimpleExoPlayer.java | 16 +- .../exoplayer2/extractor/GaplessInfo.java | 90 +++++ .../extractor/GaplessInfoHolder.java | 81 +--- .../exoplayer2/extractor/mp3/Id3Util.java | 242 ++---------- .../extractor/mp3/Mp3Extractor.java | 34 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 370 ++++++++++++++++-- .../extractor/mp4/Mp4Extractor.java | 22 +- .../android/exoplayer2/metadata/Metadata.java | 105 +++++ .../exoplayer2/metadata/MetadataBuilder.java | 42 ++ .../exoplayer2/metadata/id3/ApicFrame.java | 62 +++ .../exoplayer2/metadata/id3/BinaryFrame.java | 49 +++ .../exoplayer2/metadata/id3/CommentFrame.java | 83 ++++ .../exoplayer2/metadata/id3/GeobFrame.java | 63 +++ .../exoplayer2/metadata/id3/Id3Decoder.java | 273 +++++++++++-- .../exoplayer2/metadata/id3/Id3Frame.java | 14 +- .../exoplayer2/metadata/id3/PrivFrame.java | 52 +++ .../metadata/id3/TextInformationFrame.java | 47 +++ .../exoplayer2/metadata/id3/TxxxFrame.java | 52 +++ .../upstream/ContentDataSource.java | 24 +- .../exoplayer2/util/ParsableByteArray.java | 18 + 23 files changed, 1386 insertions(+), 437 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index d52fc30cba..4cb0bf12bb 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -27,8 +27,11 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; @@ -54,7 +57,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output> { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -173,10 +176,11 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output> + // MetadataRenderer.Output @Override - public void onMetadata(List id3Frames) { + public void onMetadata(Metadata metadata) { + List id3Frames = metadata.getFrames(); for (Id3Frame id3Frame : id3Frames) { if (id3Frame instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) id3Frame; @@ -197,10 +201,19 @@ import java.util.Locale; TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, textInformationFrame.description)); + } else if (id3Frame instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) id3Frame; + Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, + commentFrame.language, commentFrame.text)); } else { Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); } } + GaplessInfo gaplessInfo = metadata.getGaplessInfo(); + if (gaplessInfo != null) { + Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d", + gaplessInfo.encoderDelay, gaplessInfo.encoderPadding)); + } } // AudioRendererEventListener diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index f9ec1ee92b..97ebc6dbbc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.test.MoreAsserts; import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.Metadata; import java.util.List; import junit.framework.TestCase; @@ -30,7 +31,8 @@ public class Id3DecoderTest extends TestCase { 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); assertEquals("", txxxFrame.description); @@ -42,7 +44,8 @@ public class Id3DecoderTest extends TestCase { 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); assertEquals("image/jpeg", apicFrame.mimeType); @@ -56,7 +59,8 @@ public class Id3DecoderTest extends TestCase { byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0, 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; Id3Decoder decoder = new Id3Decoder(); - List id3Frames = decoder.decode(rawId3, rawId3.length); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + List id3Frames = metadata.getFrames(); assertEquals(1, id3Frames.size()); TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); assertEquals("TIT2", textInformationFrame.id); diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 9cfe019ef4..133569809b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,6 +21,8 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; @@ -100,6 +102,11 @@ public final class Format implements Parcelable { * DRM initialization data if the stream is protected, or null otherwise. */ public final DrmInitData drmInitData; + /** + * Static metadata + */ + public final Metadata metadata; + // Video specific. @@ -196,7 +203,7 @@ public final class Format implements Parcelable { float frameRate, List initializationData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null); + NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, @@ -222,7 +229,7 @@ public final class Format implements Parcelable { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, - drmInitData); + drmInitData, null); } // Audio. @@ -233,7 +240,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData, - null); + null, null); } public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, @@ -260,7 +267,7 @@ public final class Format implements Parcelable { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData); + initializationData, drmInitData, null); } // Text. @@ -269,7 +276,7 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null); + NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, @@ -283,7 +290,7 @@ public final class Format implements Parcelable { long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData); + NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData, null); } // Image. @@ -292,7 +299,7 @@ public final class Format implements Parcelable { int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData); + NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } // Generic. @@ -301,14 +308,14 @@ public final class Format implements Parcelable { String sampleMimeType, int bitrate) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null); + NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData); + NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, @@ -316,7 +323,7 @@ public final class Format implements Parcelable { float pixelWidthHeightRatio, byte[] projectionData, int stereoMode, int channelCount, int sampleRate, int pcmEncoding, int encoderDelay, int encoderPadding, int selectionFlags, String language, long subsampleOffsetUs, List initializationData, - DrmInitData drmInitData) { + DrmInitData drmInitData, Metadata metadata) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -341,6 +348,7 @@ public final class Format implements Parcelable { this.initializationData = initializationData == null ? Collections.emptyList() : initializationData; this.drmInitData = drmInitData; + this.metadata = metadata; } /* package */ Format(Parcel in) { @@ -372,20 +380,21 @@ public final class Format implements Parcelable { initializationData.add(in.createByteArray()); } drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); + metadata = in.readParcelable(Metadata.class.getClassLoader()); } public Format copyWithMaxInputSize(int maxInputSize) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithContainerInfo(String id, int bitrate, int width, int height, @@ -393,7 +402,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithManifestFormatInfo(Format manifestFormat, @@ -409,21 +418,32 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData); + language, subsampleOffsetUs, initializationData, drmInitData, null); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData); + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + } + + public Format copyWithMetadata(Metadata metadata) { + GaplessInfo gaplessInfo = metadata.getGaplessInfo(); + int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay; + int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding; + + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, + width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, + stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } /** @@ -483,6 +503,7 @@ public final class Format implements Parcelable { result = 31 * result + sampleRate; result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); + result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); hashCode = result; } return hashCode; @@ -510,6 +531,7 @@ public final class Format implements Parcelable { || !Util.areEqual(sampleMimeType, other.sampleMimeType) || !Util.areEqual(codecs, other.codecs) || !Util.areEqual(drmInitData, other.drmInitData) + || !Util.areEqual(metadata, other.metadata) || !Arrays.equals(projectionData, other.projectionData) || initializationData.size() != other.initializationData.size()) { return false; @@ -582,6 +604,7 @@ public final class Format implements Parcelable { dest.writeByteArray(initializationData.get(i)); } dest.writeParcelable(drmInitData, 0); + dest.writeParcelable(metadata, 0); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 18f8fc3942..b58bb95438 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -31,9 +31,9 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; @@ -100,7 +100,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private SurfaceHolder surfaceHolder; private TextRenderer.Output textOutput; - private MetadataRenderer.Output> id3Output; + private MetadataRenderer.Output id3Output; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -345,7 +345,7 @@ public final class SimpleExoPlayer implements ExoPlayer { * * @param output The output. */ - public void setId3Output(MetadataRenderer.Output> output) { + public void setId3Output(MetadataRenderer.Output output) { id3Output = output; } @@ -484,7 +484,7 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); renderersList.add(textRenderer); - MetadataRenderer> id3Renderer = new MetadataRenderer<>(componentListener, + MetadataRenderer id3Renderer = new MetadataRenderer<>(componentListener, mainHandler.getLooper(), new Id3Decoder()); renderersList.add(id3Renderer); } @@ -565,7 +565,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output>, + AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback { // VideoRendererEventListener implementation @@ -696,12 +696,12 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - // MetadataRenderer.Output> implementation + // MetadataRenderer.Output implementation @Override - public void onMetadata(List id3Frames) { + public void onMetadata(Metadata metadata) { if (id3Output != null) { - id3Output.onMetadata(id3Frames); + id3Output.onMetadata(metadata); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java new file mode 100644 index 0000000000..7335d9103f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.extractor; + +import android.util.Log; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Gapless playback information. + */ +public final class GaplessInfo { + + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); + + /** + * The number of samples to trim from the start of the decoded audio stream. + */ + public final int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream. + */ + public final int encoderPadding; + + /** + * Parses gapless playback information from a gapless playback comment (stored in an ID3 header + * or MPEG 4 user data), if valid and non-zero. + * @param name The comment's identifier. + * @param data The comment's payload data. + * @return the gapless playback info, or null if the provided data is not valid. + */ + public static GaplessInfo createFromComment(String name, String data) { + if(!GAPLESS_COMMENT_ID.equals(name)) { + return null; + } else { + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if(matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if(encoderDelay > 0 || encoderPadding > 0) { + Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding); + return new GaplessInfo(encoderDelay, encoderPadding); + } + } catch (NumberFormatException var5) { + ; + } + } + + // Ignore incorrectly formatted comments. + Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data); + return null; + } + } + + /** + * Parses gapless playback information from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return the gapless playback info, or null if the provided data is not valid. + */ + public static GaplessInfo createFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + return encoderDelay > 0 || encoderPadding > 0 ? + new GaplessInfo(encoderDelay, encoderPadding) : + null; + } + + public GaplessInfo(int encoderDelay, int encoderPadding) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 6eb9bc50de..4f98ce4f7e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -15,90 +15,11 @@ */ package com.google.android.exoplayer2.extractor; -import com.google.android.exoplayer2.Format; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Holder for gapless playback information. */ public final class GaplessInfoHolder { - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; - private static final Pattern GAPLESS_COMMENT_PATTERN = - Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); - - /** - * The number of samples to trim from the start of the decoded audio stream, or - * {@link Format#NO_VALUE} if not set. - */ - public int encoderDelay; - - /** - * The number of samples to trim from the end of the decoded audio stream, or - * {@link Format#NO_VALUE} if not set. - */ - public int encoderPadding; - - /** - * Creates a new holder for gapless playback information. - */ - public GaplessInfoHolder() { - encoderDelay = Format.NO_VALUE; - encoderPadding = Format.NO_VALUE; - } - - /** - * Populates the holder with data from an MP3 Xing header, if valid and non-zero. - * - * @param value The 24-bit value to decode. - * @return Whether the holder was populated. - */ - public boolean setFromXingHeaderValue(int value) { - int encoderDelay = value >> 12; - int encoderPadding = value & 0x0FFF; - if (encoderDelay > 0 || encoderPadding > 0) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - return true; - } - return false; - } - - /** - * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header - * or MPEG 4 user data), if valid and non-zero. - * - * @param name The comment's identifier. - * @param data The comment's payload data. - * @return Whether the holder was populated. - */ - public boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } - Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); - if (matcher.find()) { - try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); - if (encoderDelay > 0 || encoderPadding > 0) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - return true; - } - } catch (NumberFormatException e) { - // Ignore incorrectly formatted comments. - } - } - return false; - } - - /** - * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. - */ - public boolean hasGaplessInfo() { - return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; - } + public GaplessInfo gaplessInfo; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java index 53f18df844..af08514889 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.util.Pair; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.nio.charset.Charset; /** * Utility for parsing ID3 version 2 metadata in MP3 files. @@ -34,19 +34,18 @@ import java.nio.charset.Charset; private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"), - Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")}; /** - * Peeks data from the input and parses ID3 metadata. + * Peeks data from the input and parses ID3 metadata, including gapless playback information. * * @param input The {@link ExtractorInput} from which data should be peeked. - * @param out The {@link GaplessInfoHolder} to populate. + * @return The metadata, if present, {@code null} otherwise. * @throws IOException If an error occurred peeking from the input. * @throws InterruptedException If the thread was interrupted. */ - public static void parseId3(ExtractorInput input, GaplessInfoHolder out) + public static Metadata parseId3(ExtractorInput input) throws IOException, InterruptedException { + Metadata result = null; ParsableByteArray scratch = new ParsableByteArray(10); int peekedId3Bytes = 0; while (true) { @@ -60,18 +59,26 @@ import java.nio.charset.Charset; int minorVersion = scratch.readUnsignedByte(); int flags = scratch.readUnsignedByte(); int length = scratch.readSynchSafeInt(); - if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) { - byte[] frame = new byte[length]; - input.peekFully(frame, 0, length); - parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out); - } else { - input.advancePeekPosition(length); + int frameLength = length + 10; + + try { + if (canParseMetadata(majorVersion, minorVersion, flags, length)) { + input.resetPeekPosition(); + byte[] frame = new byte[frameLength]; + input.peekFully(frame, 0, frameLength); + return new Id3Decoder().decode(frame, frameLength); + } else { + input.advancePeekPosition(length); + } + } catch (MetadataDecoderException e) { + e.printStackTrace(); } - peekedId3Bytes += 10 + length; + peekedId3Bytes += frameLength; } input.resetPeekPosition(); input.advancePeekPosition(peekedId3Bytes); + return result; } private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, @@ -83,211 +90,6 @@ import java.nio.charset.Charset; && !(majorVersion == 4 && (flags & 0x0F) != 0); } - private static void parseGaplessInfo(ParsableByteArray frame, int version, int flags, - GaplessInfoHolder out) { - unescape(frame, version, flags); - - // Skip any extended header. - frame.setPosition(0); - if (version == 3 && (flags & 0x40) != 0) { - if (frame.bytesLeft() < 4) { - return; - } - int extendedHeaderSize = frame.readUnsignedIntToInt(); - if (extendedHeaderSize > frame.bytesLeft()) { - return; - } - int paddingSize; - if (extendedHeaderSize >= 6) { - frame.skipBytes(2); // extended flags - paddingSize = frame.readUnsignedIntToInt(); - frame.setPosition(4); - frame.setLimit(frame.limit() - paddingSize); - if (frame.bytesLeft() < extendedHeaderSize) { - return; - } - } - frame.skipBytes(extendedHeaderSize); - } else if (version == 4 && (flags & 0x40) != 0) { - if (frame.bytesLeft() < 4) { - return; - } - int extendedHeaderSize = frame.readSynchSafeInt(); - if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) { - return; - } - frame.setPosition(extendedHeaderSize); - } - - // Extract gapless playback metadata stored in comments. - Pair comment; - while ((comment = findNextComment(version, frame)) != null) { - if (comment.first.length() > 3) { - if (out.setFromComment(comment.first.substring(3), comment.second)) { - break; - } - } - } - } - - private static Pair findNextComment(int majorVersion, ParsableByteArray data) { - int frameSize; - while (true) { - if (majorVersion == 2) { - if (data.bytesLeft() < 6) { - return null; - } - String id = data.readString(3, Charset.forName("US-ASCII")); - if (id.equals("\0\0\0")) { - return null; - } - frameSize = data.readUnsignedInt24(); - if (frameSize == 0 || frameSize > data.bytesLeft()) { - return null; - } - if (id.equals("COM")) { - break; - } - } else /* major == 3 || major == 4 */ { - if (data.bytesLeft() < 10) { - return null; - } - String id = data.readString(4, Charset.forName("US-ASCII")); - if (id.equals("\0\0\0\0")) { - return null; - } - frameSize = majorVersion == 4 ? data.readSynchSafeInt() : data.readUnsignedIntToInt(); - if (frameSize == 0 || frameSize > data.bytesLeft() - 2) { - return null; - } - int flags = data.readUnsignedShort(); - boolean compressedOrEncrypted = (majorVersion == 4 && (flags & 0x0C) != 0) - || (majorVersion == 3 && (flags & 0xC0) != 0); - if (!compressedOrEncrypted && id.equals("COMM")) { - break; - } - } - data.skipBytes(frameSize); - } - - // The comment tag is at the reading position in data. - int encoding = data.readUnsignedByte(); - if (encoding < 0 || encoding >= CHARSET_BY_ENCODING.length) { - return null; - } - Charset charset = CHARSET_BY_ENCODING[encoding]; - String[] commentFields = data.readString(frameSize - 1, charset).split("\0"); - return commentFields.length == 2 ? Pair.create(commentFields[0], commentFields[1]) : null; - } - - private static boolean unescape(ParsableByteArray frame, int version, int flags) { - if (version != 4) { - if ((flags & 0x80) != 0) { - // Remove unsynchronization on ID3 version < 2.4.0. - byte[] bytes = frame.data; - int newLength = bytes.length; - for (int i = 0; i + 1 < newLength; i++) { - if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { - System.arraycopy(bytes, i + 2, bytes, i + 1, newLength - i - 2); - newLength--; - } - } - frame.setLimit(newLength); - } - } else { - // Remove unsynchronization on ID3 version 2.4.0. - if (canUnescapeVersion4(frame, false)) { - unescapeVersion4(frame, false); - } else if (canUnescapeVersion4(frame, true)) { - unescapeVersion4(frame, true); - } else { - return false; - } - } - return true; - } - - private static boolean canUnescapeVersion4(ParsableByteArray frame, - boolean unsignedIntDataSizeHack) { - frame.setPosition(0); - while (frame.bytesLeft() >= 10) { - if (frame.readInt() == 0) { - return true; - } - long dataSize = frame.readUnsignedInt(); - if (!unsignedIntDataSizeHack) { - // Parse the data size as a syncsafe integer. - if ((dataSize & 0x808080L) != 0) { - return false; - } - dataSize = (dataSize & 0x7F) | (((dataSize >> 8) & 0x7F) << 7) - | (((dataSize >> 16) & 0x7F) << 14) | (((dataSize >> 24) & 0x7F) << 21); - } - if (dataSize > frame.bytesLeft() - 2) { - return false; - } - int flags = frame.readUnsignedShort(); - if ((flags & 1) != 0) { - if (frame.bytesLeft() < 4) { - return false; - } - } - frame.skipBytes((int) dataSize); - } - return true; - } - - private static void unescapeVersion4(ParsableByteArray frame, boolean unsignedIntDataSizeHack) { - frame.setPosition(0); - byte[] bytes = frame.data; - while (frame.bytesLeft() >= 10) { - if (frame.readInt() == 0) { - return; - } - int dataSize = - unsignedIntDataSizeHack ? frame.readUnsignedIntToInt() : frame.readSynchSafeInt(); - int flags = frame.readUnsignedShort(); - int previousFlags = flags; - if ((flags & 1) != 0) { - // Strip data length indicator. - int offset = frame.getPosition(); - System.arraycopy(bytes, offset + 4, bytes, offset, frame.bytesLeft() - 4); - dataSize -= 4; - flags &= ~1; - frame.setLimit(frame.limit() - 4); - } - if ((flags & 2) != 0) { - // Unescape 0xFF00 to 0xFF in the next dataSize bytes. - int readOffset = frame.getPosition() + 1; - int writeOffset = readOffset; - for (int i = 0; i + 1 < dataSize; i++) { - if ((bytes[readOffset - 1] & 0xFF) == 0xFF && bytes[readOffset] == 0) { - readOffset++; - dataSize--; - } - bytes[writeOffset++] = bytes[readOffset++]; - } - frame.setLimit(frame.limit() - (readOffset - writeOffset)); - System.arraycopy(bytes, readOffset, bytes, writeOffset, frame.bytesLeft() - readOffset); - flags &= ~2; - } - if (flags != previousFlags || unsignedIntDataSizeHack) { - int dataSizeOffset = frame.getPosition() - 6; - writeSyncSafeInteger(bytes, dataSizeOffset, dataSize); - bytes[dataSizeOffset + 4] = (byte) (flags >> 8); - bytes[dataSizeOffset + 5] = (byte) (flags & 0xFF); - } - frame.skipBytes(dataSize); - } - } - - private static void writeSyncSafeInteger(byte[] bytes, int offset, int value) { - bytes[offset] = (byte) ((value >> 21) & 0x7F); - bytes[offset + 1] = (byte) ((value >> 14) & 0x7F); - bytes[offset + 2] = (byte) ((value >> 7) & 0x7F); - bytes[offset + 3] = (byte) (value & 0x7F); - } - private Id3Util() {} } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index fdd037cde3..2004c0de19 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -22,11 +22,12 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -69,7 +70,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private final GaplessInfoHolder gaplessInfoHolder; + private Metadata metadata; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -99,7 +100,6 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); - gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; } @@ -137,10 +137,21 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null)); + + GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null; + + Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, + synchronizedHeader.sampleRate, Format.NO_VALUE, + gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, + gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE, + null, null, 0, null); + + if (metadata != null) { + format = format.copyWithMetadata(metadata); + } + + trackOutput.format(format); } return readSample(input); } @@ -220,7 +231,7 @@ public final class Mp3Extractor implements Extractor { int peekedId3Bytes = 0; input.resetPeekPosition(); if (input.getPosition() == 0) { - Id3Util.parseId3(input, gaplessInfoHolder); + metadata = Id3Util.parseId3(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -303,13 +314,16 @@ public final class Mp3Extractor implements Extractor { Seeker seeker = null; if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); - if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { + if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); - gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); + GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); + metadata = metadata != null ? + metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo); + } input.skipFully(synchronizedHeader.frameSize); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 11a25fe419..cfa56ba3da 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -21,7 +21,15 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataBuilder; +import com.google.android.exoplayer2.metadata.id3.BinaryFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -270,7 +278,7 @@ import java.util.List; flags = rechunkedResults.flags; } - if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { + if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); @@ -299,10 +307,9 @@ import java.util.List; track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale); - if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE + if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) { - gaplessInfoHolder.encoderDelay = (int) encoderDelay; - gaplessInfoHolder.encoderPadding = (int) encoderPadding; + gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } @@ -387,17 +394,17 @@ import java.util.List; } /** - * Parses a udta atom. + * Parses a udta atom for metadata, including gapless playback information. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @param out {@link GaplessInfoHolder} to populate with gapless playback information. + * @return metadata stored in the user data, or {@code null} if not present. */ - public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. - return; + return null; } ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); @@ -407,14 +414,15 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - parseMetaAtom(udtaData, out); + parseMetaAtom(udtaData); break; } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } + return null; } - private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { + private static Metadata parseMetaAtom(ParsableByteArray data) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -423,47 +431,333 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - parseIlst(ilst, out); - if (out.hasGaplessInfo()) { - return; + Metadata result = parseIlst(ilst); + if (result != null) { + return result; } } data.skipBytes(payloadSize); } + return null; } - private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + private static Metadata parseIlst(ParsableByteArray ilst) { + + MetadataBuilder builder = new MetadataBuilder(); + while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - if (type == Atom.TYPE_DASHES) { - String lastCommentMean = null; - String lastCommentName = null; - String lastCommentData = null; - while (ilst.getPosition() < endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_mean) { - lastCommentMean = ilst.readString(length); - } else if (key == Atom.TYPE_name) { - lastCommentName = ilst.readString(length); - } else if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - lastCommentData = ilst.readString(length - 4); - } else { - ilst.skipBytes(length); + parseIlstElement(ilst, type, endPosition, builder); + ilst.setPosition(endPosition); + } + + return builder.build(); + } + + private static final String P1 = "\u00a9"; + private static final String P2 = "\ufffd"; + private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam"); + private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam"); + private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk"); + private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk"); + private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt"); + private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt"); + private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day"); + private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day"); + private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART"); + private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART"); + private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too"); + private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too"); + private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb"); + private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb"); + private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com"); + private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com"); + private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt"); + private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt"); + private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr"); + private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr"); + private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen"); + private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen"); + private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre"); + private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp"); + private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "grp"); + private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); + private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); + private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); + private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); + private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); + private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); + private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); + private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); + private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); + private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn"); + private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); + private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh"); + + // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes + + private static void parseIlstElement( + ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) { + if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { + parseTextAttribute(builder, "TIT2", ilst, endPosition); + } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { + parseCommentAttribute(builder, "COMM", ilst, endPosition); + } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { + parseTextAttribute(builder, "TDRC", ilst, endPosition); + } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { + parseTextAttribute(builder, "TPE1", ilst, endPosition); + } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { + parseTextAttribute(builder, "TSSE", ilst, endPosition); + } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { + parseTextAttribute(builder, "TALB", ilst, endPosition); + } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || + type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { + parseTextAttribute(builder, "TCOM", ilst, endPosition); + } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { + parseTextAttribute(builder, "lyrics", ilst, endPosition); + } else if (type == TYPE_STANDARD_GENRE) { + parseStandardGenreAttribute(builder, "TCON", ilst, endPosition); + } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { + parseTextAttribute(builder, "TCON", ilst, endPosition); + } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { + parseTextAttribute(builder, "TIT1", ilst, endPosition); + } else if (type == TYPE_DISK_NUMBER) { + parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); + } else if (type == TYPE_TRACK_NUMBER) { + parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); + } else if (type == TYPE_TEMPO) { + parseIntegerAttribute(builder, "TBPM", ilst, endPosition); + } else if (type == TYPE_COMPILATION) { + parseBooleanAttribute(builder, "TCMP", ilst, endPosition); + } else if (type == TYPE_ALBUM_ARTIST) { + parseTextAttribute(builder, "TPE2", ilst, endPosition); + } else if (type == TYPE_SORT_TRACK_NAME) { + parseTextAttribute(builder, "TSOT", ilst, endPosition); + } else if (type == TYPE_SORT_ALBUM) { + parseTextAttribute(builder, "TSO2", ilst, endPosition); + } else if (type == TYPE_SORT_ARTIST) { + parseTextAttribute(builder, "TSOA", ilst, endPosition); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + parseTextAttribute(builder, "TSOP", ilst, endPosition); + } else if (type == TYPE_SORT_COMPOSER) { + parseTextAttribute(builder, "TSOC", ilst, endPosition); + } else if (type == TYPE_SORT_SHOW) { + parseTextAttribute(builder, "sortShow", ilst, endPosition); + } else if (type == TYPE_GAPLESS_ALBUM) { + parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition); + } else if (type == TYPE_SHOW) { + parseTextAttribute(builder, "show", ilst, endPosition); + } else if (type == Atom.TYPE_DASHES) { + parseExtendedAttribute(builder, ilst, endPosition); + } + } + + private static void parseTextAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + ilst.skipBytes(4); + String value = ilst.readNullTerminatedString(length - 4); + Id3Frame frame = new TextInformationFrame(attributeName, value); + builder.add(frame); + } else { + ilst.skipBytes(length); + } + } + + private static void parseCommentAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + ilst.skipBytes(4); + String value = ilst.readNullTerminatedString(length - 4); + Id3Frame frame = new CommentFrame("eng", attributeName, value); + builder.add(frame); + } else { + ilst.skipBytes(length); + } + } + + private static void parseBooleanAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof Integer) { + int n = (Integer) value; + String s = n == 0 ? "0" : "1"; + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseIntegerAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof Integer) { + int n = (Integer) value; + String s = "" + n; + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseIndexAndCountAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + if (bytes.length == 8) { + int index = (bytes[2] << 8) + (bytes[3] & 0xFF); + int count = (bytes[4] << 8) + (bytes[5] & 0xFF); + if (index > 0) { + String s = "" + index; + if (count > 0) { + s = s + "/" + count; + } + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); } } - if (lastCommentName != null && lastCommentData != null - && "com.apple.iTunes".equals(lastCommentMean)) { - out.setFromComment(lastCommentName, lastCommentData); - break; - } - } else { - ilst.setPosition(endPosition); } + } else { + ilst.skipBytes(length); + } + } + + private static void parseStandardGenreAttribute(MetadataBuilder builder, + String attributeName, + ParsableByteArray ilst, + int endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_data) { + Object value = parseDataBox(ilst, length); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + if (bytes.length == 2) { + int code = (bytes[0] << 8) + (bytes[1] & 0xFF); + String s = Id3Decoder.decodeGenre(code); + if (s != null) { + Id3Frame frame = new TextInformationFrame(attributeName, s); + builder.add(frame); + } + } + } + } else { + ilst.skipBytes(length); + } + } + + private static void parseExtendedAttribute(MetadataBuilder builder, + ParsableByteArray ilst, + int endPosition) { + String domain = null; + String name = null; + Object value = null; + + while (ilst.getPosition() < endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_mean) { + domain = ilst.readNullTerminatedString(length); + } else if (key == Atom.TYPE_name) { + name = ilst.readNullTerminatedString(length); + } else if (key == Atom.TYPE_data) { + value = parseDataBox(ilst, length); + } else { + ilst.skipBytes(length); + } + } + + if (value != null) { + if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) { + String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); + builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s)); + } + + if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { + String s = new String((byte[]) value); + Id3Frame frame = new CommentFrame("eng", "iTunNORM", s); + builder.add(frame); + } else if (domain != null && name != null) { + String extendedName = domain + "." + name; + if (value instanceof String) { + Id3Frame frame = new TextInformationFrame(extendedName, (String) value); + builder.add(frame); + } else if (value instanceof Integer) { + Id3Frame frame = new TextInformationFrame(extendedName, value.toString()); + builder.add(frame); + } else if (value instanceof byte[]) { + byte[] bb = (byte[]) value; + Id3Frame frame = new BinaryFrame(extendedName, bb); + builder.add(frame); + } + } + } + } + + private static Object parseDataBox(ParsableByteArray ilst, int length) { + int versionAndFlags = ilst.readInt(); + int flags = versionAndFlags & 0xFFFFFF; + boolean isText = (flags == 1); + boolean isData = (flags == 0); + boolean isImageData = (flags == 0xD); + boolean isInteger = (flags == 21); + int dataLength = length - 4; + if (isText) { + return ilst.readNullTerminatedString(dataLength); + } else if (isInteger) { + if (dataLength == 1) { + return ilst.readUnsignedByte(); + } else if (dataLength == 2) { + return ilst.readUnsignedShort(); + } else { + ilst.skipBytes(dataLength); + return null; + } + } else if (isData) { + byte[] bytes = new byte[dataLength]; + ilst.readBytes(bytes, 0, dataLength); + return bytes; + } else { + ilst.skipBytes(dataLength); + return null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 467ec7a4fa..1f4461f21e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -22,11 +22,13 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -309,11 +311,16 @@ public final class Mp4Extractor implements Extractor, SeekMap { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; + GaplessInfo gaplessInfo = null; + Metadata metadata = null; - GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); + Metadata info = AtomParsers.parseUdta(udta, isQuickTime); + if (info != null) { + gaplessInfo = info.getGaplessInfo(); + metadata = info; + } } for (int i = 0; i < moov.containerChildren.size(); i++) { @@ -330,7 +337,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); + gaplessInfoHolder.gaplessInfo = gaplessInfo; TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + gaplessInfo = gaplessInfoHolder.gaplessInfo; if (trackSampleTable.sampleCount == 0) { continue; } @@ -340,9 +350,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); + if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { + format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + } + if (metadata != null) { + format = format.copyWithMetadata(metadata); } mp4Track.trackOutput.format(format); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java new file mode 100644 index 0000000000..c30e7ddb57 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016 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; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * ID3 style metadata, with convenient access to gapless playback information. + */ +public class Metadata implements Parcelable { + + private final List frames; + private final GaplessInfo gaplessInfo; + + public Metadata(List frames, GaplessInfo gaplessInfo) { + List theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList(); + this.frames = Collections.unmodifiableList(theFrames); + this.gaplessInfo = gaplessInfo; + } + + public Metadata(Parcel in) { + int encoderDelay = in.readInt(); + int encoderPadding = in.readInt(); + gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? + new GaplessInfo(encoderDelay, encoderPadding) : null; + frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); + } + + public Metadata withGaplessInfo(GaplessInfo info) { + return new Metadata(frames, info); + } + + public List getFrames() { + return frames; + } + + public GaplessInfo getGaplessInfo() { + return gaplessInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Metadata that = (Metadata) o; + + if (!frames.equals(that.frames)) return false; + return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; + } + + @Override + public int hashCode() { + int result = frames.hashCode(); + result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0); + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); + dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); + dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + + @Override + public Metadata[] newArray(int size) { + return new Metadata[0]; + } + }; +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java new file mode 100644 index 0000000000..57f49e5b20 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 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; + +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for ID3 style metadata. + */ +public class MetadataBuilder { + private List frames = new ArrayList<>(); + private GaplessInfo gaplessInfo; + + public void add(Id3Frame frame) { + frames.add(frame); + } + + public void setGaplessInfo(GaplessInfo info) { + this.gaplessInfo = info; + } + + public Metadata build() { + return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index d2a04bdb94..9acb6840a7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * APIC (Attached Picture) ID3 frame. */ @@ -35,4 +39,62 @@ public final class ApicFrame extends Id3Frame { this.pictureData = pictureData; } + public ApicFrame(Parcel in) { + super(in); + mimeType = in.readString(); + description = in.readString(); + pictureType = in.readInt(); + pictureData = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ApicFrame that = (ApicFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (pictureType != that.pictureType) return false; + if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return Arrays.equals(pictureData, that.pictureData); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + pictureType; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(pictureType); + dest.writeByteArray(pictureData); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public ApicFrame createFromParcel(Parcel in) { + return new ApicFrame(in); + } + + @Override + public ApicFrame[] newArray(int size) { + return new ApicFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index 5bc4ce3829..a07bdf5934 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * Binary ID3 frame. */ @@ -27,4 +31,49 @@ public final class BinaryFrame extends Id3Frame { this.data = data; } + public BinaryFrame(Parcel in) { + super(in); + data = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BinaryFrame that = (BinaryFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) + return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public BinaryFrame createFromParcel(Parcel in) { + return new BinaryFrame(in); + } + + @Override + public BinaryFrame[] newArray(int size) { + return new BinaryFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java new file mode 100644 index 0000000000..53b3b8212a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 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.os.Parcelable; + +/** + * Comment ID3 frame. + */ +public final class CommentFrame extends Id3Frame { + + public final String language; + public final String text; + + public CommentFrame(String language, String description, String text) { + super(description); + this.language = language; + this.text = text; + } + + public CommentFrame(Parcel in) { + super(in); + language = in.readString(); + text = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CommentFrame that = (CommentFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (language != null ? !language.equals(that.language) : that.language != null) return false; + return text != null ? text.equals(that.text) : that.text == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(language); + dest.writeString(text); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public CommentFrame createFromParcel(Parcel in) { + return new CommentFrame(in); + } + + @Override + public CommentFrame[] newArray(int size) { + return new CommentFrame[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 4b77a69b27..5e4aa70b14 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * GEOB (General Encapsulated Object) ID3 frame. */ @@ -35,4 +39,63 @@ public final class GeobFrame extends Id3Frame { this.data = data; } + public GeobFrame(Parcel in) { + super(in); + mimeType = in.readString(); + filename = in.readString(); + description = in.readString(); + data = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeobFrame that = (GeobFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + return false; + if (filename != null ? !filename.equals(that.filename) : that.filename != null) + return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (filename != null ? filename.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(mimeType); + dest.writeString(filename); + dest.writeString(description); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public GeobFrame createFromParcel(Parcel in) { + return new GeobFrame(in); + } + + @Override + public GeobFrame[] newArray(int size) { + return new GeobFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 92c6efb530..833162ab77 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.metadata.id3; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.MimeTypes; @@ -23,69 +25,141 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Locale; /** * Decodes individual TXXX text frames from raw ID3 data. */ -public final class Id3Decoder implements MetadataDecoder> { +public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + private int majorVersion; + private int minorVersion; + private boolean isUnsynchronized; + private GaplessInfo gaplessInfo; + @Override public boolean canDecode(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); } @Override - public List decode(byte[] data, int size) throws MetadataDecoderException { + public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); int id3Size = decodeId3Header(id3Data); + if (isUnsynchronized) { + id3Data = removeUnsynchronization(id3Data, id3Size); + id3Size = id3Data.bytesLeft(); + } + while (id3Size > 0) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Data.readUnsignedByte(); - int frameSize = id3Data.readSynchSafeInt(); + int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0; + int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() : + majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + if (frameSize <= 1) { break; } - // Skip frame flags. - id3Data.skipBytes(2); + // Frame flags. + boolean isCompressed = false; + boolean isEncrypted = false; + boolean isUnsynchronized = false; + boolean hasGroupIdentifier = false; + boolean hasDataLength = false; - try { - Id3Frame frame; - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - frame = decodeTxxxFrame(id3Data, frameSize); - } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { - frame = decodePrivFrame(id3Data, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { - frame = decodeGeobFrame(id3Data, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(id3Data, frameSize); - } else if (frameId0 == 'T') { - String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); - frame = decodeTextInformationFrame(id3Data, frameSize, id); + if (majorVersion > 2) { + int flags = id3Data.readShort(); + if (majorVersion == 3) { + isCompressed = (flags & 0x0080) != 0; + isEncrypted = (flags & 0x0040) != 0; + hasDataLength = isCompressed; } else { - String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); - frame = decodeBinaryFrame(id3Data, frameSize, id); + isCompressed = (flags & 0x0008) != 0; + isEncrypted = (flags & 0x0004) != 0; + isUnsynchronized = (flags & 0x0002) != 0; + hasGroupIdentifier = (flags & 0x0040) != 0; + hasDataLength = (flags & 0x0001) != 0; + } + } + + int headerSize = majorVersion == 2 ? 6 : 10; + + if (hasGroupIdentifier) { + ++headerSize; + --frameSize; + id3Data.skipBytes(1); + } + + if (isEncrypted) { + ++headerSize; + --frameSize; + id3Data.skipBytes(1); + } + + if (hasDataLength) { + headerSize += 4; + frameSize -= 4; + id3Data.skipBytes(4); + } + + id3Size -= frameSize + headerSize; + + if (isCompressed || isEncrypted) { + id3Data.skipBytes(frameSize); + } else { + try { + Id3Frame frame; + ParsableByteArray frameData = id3Data; + if (isUnsynchronized) { + frameData = removeUnsynchronization(id3Data, frameSize); + frameSize = frameData.bytesLeft(); + } + + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + frame = decodeTxxxFrame(frameData, frameSize); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = decodePrivFrame(frameData, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + frame = decodeGeobFrame(frameData, frameSize); + } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { + frame = decodeApicFrame(frameData, frameSize); + } else if (frameId0 == 'T') { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeTextInformationFrame(frameData, frameSize, id); + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && + (frameId3 == 'M' || frameId3 == 0)) { + CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); + frame = commentFrame; + if (gaplessInfo == null) { + gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text); + } + } else { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeBinaryFrame(frameData, frameSize, id); + } + id3Frames.add(frame); + } catch (UnsupportedEncodingException e) { + throw new MetadataDecoderException("Unsupported character encoding"); } - id3Frames.add(frame); - id3Size -= frameSize + 10 /* header size */; - } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported encoding", e); } } - return Collections.unmodifiableList(id3Frames); + return new Metadata(id3Frames, null); } private static int indexOfEos(byte[] data, int fromIndex, int encoding) { @@ -96,7 +170,7 @@ public final class Id3Decoder implements MetadataDecoder> { return terminationPos; } - // Otherwise look for a second zero byte. + // Otherwise ensure an even index and look for a second zero byte. while (terminationPos < data.length - 1) { if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { return terminationPos; @@ -126,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder> { * @return The size of ID3 frames in bytes, excluding the header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { + private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -134,23 +208,41 @@ public final class Id3Decoder implements MetadataDecoder> { throw new MetadataDecoderException(String.format(Locale.US, "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - id3Buffer.skipBytes(2); // Skip version. + + majorVersion = id3Buffer.readUnsignedByte(); + minorVersion = id3Buffer.readUnsignedByte(); int flags = id3Buffer.readUnsignedByte(); int id3Size = id3Buffer.readSynchSafeInt(); - // Check if extended header presents. - if ((flags & 0x2) != 0) { - int extendedHeaderSize = id3Buffer.readSynchSafeInt(); - if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); - } - id3Size -= extendedHeaderSize; + if (majorVersion < 4) { + // this flag is advisory in version 4, use the frame flags instead + isUnsynchronized = (flags & 0x80) != 0; } - // Check if footer presents. - if ((flags & 0x8) != 0) { - id3Size -= 10; + if (majorVersion == 3) { + // check for extended header + if ((flags & 0x40) != 0) { + int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field + if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { + id3Buffer.skipBytes(extendedHeaderSize); + id3Size -= (extendedHeaderSize + 4); + } + } + } else if (majorVersion >= 4) { + // check for extended header + if ((flags & 0x40) != 0) { + int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field + if (extendedHeaderSize > 4) { + id3Buffer.skipBytes(extendedHeaderSize - 4); + } + id3Size -= extendedHeaderSize; + } + + // Check if footer presents. + if ((flags & 0x10) != 0) { + id3Size -= 10; + } } return id3Size; @@ -253,6 +345,28 @@ public final class Id3Decoder implements MetadataDecoder> { return new TextInformationFrame(id, description); } + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, + int frameSize) throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[3]; + id3Data.readBytes(data, 0, 3); + String language = new String(data, 0, 3); + + data = new byte[frameSize - 4]; + id3Data.readBytes(data, 0, frameSize - 4); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + + return new CommentFrame(language, description, value); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; @@ -261,6 +375,37 @@ public final class Id3Decoder implements MetadataDecoder> { return new BinaryFrame(id, frame); } + /** + * Undo the unsynchronization applied to one or more frames. + * @param dataSource The original data, positioned at the beginning of a frame. + * @param count The number of valid bytes in the frames to be processed. + * @return replacement data for the frames. + */ + private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) { + byte[] source = dataSource.data; + int sourceIndex = dataSource.getPosition(); + int limit = sourceIndex + count; + byte[] dest = new byte[count]; + int destIndex = 0; + + while (sourceIndex < limit) { + byte b = source[sourceIndex++]; + if ((b & 0xFF) == 0xFF) { + int nextIndex = sourceIndex+1; + if (nextIndex < limit) { + int b2 = source[nextIndex]; + if (b2 == 0) { + // skip the 0 byte + ++sourceIndex; + } + } + } + dest[destIndex++] = b; + } + + return new ParsableByteArray(dest, destIndex); + } + /** * Maps encoding byte from ID3v2 frame to a Charset. * @param encodingByte The value of encoding byte from ID3v2 frame. @@ -281,4 +426,52 @@ public final class Id3Decoder implements MetadataDecoder> { } } + private final static String[] standardGenres = new String[] { + + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + public static String decodeGenre(int n) + { + n--; + + if (n < 0 || n >= standardGenres.length) { + return null; + } + + return standardGenres[n]; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 903b32da4f..ea4776d784 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * Base class for ID3 frames. */ -public abstract class Id3Frame { +public abstract class Id3Frame implements Parcelable { /** * The frame ID. @@ -29,4 +32,13 @@ public abstract class Id3Frame { this.id = id; } + protected Id3Frame(Parcel in) { + id = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index bbfbd96b84..b0f9cb528f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + /** * PRIV (Private) ID3 frame. */ @@ -31,4 +35,52 @@ public final class PrivFrame extends Id3Frame { this.privateData = privateData; } + public PrivFrame(Parcel in) { + super(in); + owner = in.readString(); + privateData = in.createByteArray(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrivFrame that = (PrivFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false; + return Arrays.equals(privateData, that.privateData); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + Arrays.hashCode(privateData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(owner); + dest.writeByteArray(privateData); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PrivFrame createFromParcel(Parcel in) { + return new PrivFrame(in); + } + + @Override + public PrivFrame[] newArray(int size) { + return new PrivFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index ec05a8ff4b..3c6409ca7d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. */ @@ -27,4 +30,48 @@ public final class TextInformationFrame extends Id3Frame { this.description = description; } + public TextInformationFrame(Parcel in) { + super(in); + description = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TextInformationFrame that = (TextInformationFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + return description != null ? description.equals(that.description) : that.description == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TextInformationFrame createFromParcel(Parcel in) { + return new TextInformationFrame(in); + } + + @Override + public TextInformationFrame[] newArray(int size) { + return new TextInformationFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java index 6593c2f120..25ff1e063d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.os.Parcel; +import android.os.Parcelable; + /** * TXXX (User defined text information) ID3 frame. */ @@ -31,4 +34,53 @@ public final class TxxxFrame extends Id3Frame { this.value = value; } + public TxxxFrame(Parcel in) { + super(in); + description = in.readString(); + value = in.readString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TxxxFrame that = (TxxxFrame) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (description != null ? !description.equals(that.description) : that.description != null) + return false; + return value != null ? value.equals(that.value) : that.value == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(description); + dest.writeString(value); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TxxxFrame createFromParcel(Parcel in) { + return new TxxxFrame(in); + } + + @Override + public TxxxFrame[] newArray(int size) { + return new TxxxFrame[size]; + } + + }; + } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 2a0ad01489..06242216ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -136,22 +136,13 @@ public final class ContentDataSource implements DataSource { @Override public void close() throws ContentDataSourceException { uri = null; - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - throw new ContentDataSourceException(e); - } finally { - inputStream = null; + if (inputStream != null) { try { - if (assetFileDescriptor != null) { - assetFileDescriptor.close(); - } + inputStream.close(); } catch (IOException e) { throw new ContentDataSourceException(e); } finally { - assetFileDescriptor = null; + inputStream = null; if (opened) { opened = false; if (listener != null) { @@ -160,6 +151,13 @@ public final class ContentDataSource implements DataSource { } } } - } + if (assetFileDescriptor != null) { + try { + assetFileDescriptor.close(); + } catch (Exception e) { + } + assetFileDescriptor = null; + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index a499dc8012..5361288263 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -416,6 +416,24 @@ public final class ParsableByteArray { return readString(length, Charset.defaultCharset()); } + /** + * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored, + * if present. + * + * @param length The number of bytes to read. + * @return The string encoded by the bytes. + */ + public String readNullTerminatedString(int length) { + int stringLength = length; + int lastIndex = position + length - 1; + if (lastIndex < limit && data[lastIndex] == 0) { + stringLength--; + } + String result = new String(data, position, stringLength, Charset.defaultCharset()); + position += length; + return result; + } + /** * Reads the next {@code length} bytes as characters in the specified {@link Charset}. * From 776da107251e2e51f12d23986b7e83a2c39a2bdd Mon Sep 17 00:00:00 2001 From: Alan Snyder Date: Mon, 5 Sep 2016 15:39:40 -0700 Subject: [PATCH 012/206] Fix merge issue --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index cfa56ba3da..62dd11876f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -414,8 +414,7 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - parseMetaAtom(udtaData); - break; + return parseMetaAtom(udtaData); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } From b87463a85791e1e1600413cedf4c9adfd8615a64 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 14 Sep 2016 09:32:53 -0400 Subject: [PATCH 013/206] Fixed an off by one error and fixed iterating through the cues --- .../text/eia608/Eia608CueBuilder.java | 8 +++---- .../exoplayer2/text/eia608/Eia608Decoder.java | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index 3531b4dcc2..aee60cb38b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -1,8 +1,5 @@ package com.google.android.exoplayer2.text.eia608; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.util.Assertions; - import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -10,6 +7,9 @@ import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; + import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -73,7 +73,7 @@ import java.util.List; * @return true if rolling was possible. */ public boolean rollUp() { - if (row < 1) { + if (row <= 1) { return false; } setRow(row - 1); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 48fa816977..086178622b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -15,6 +15,13 @@ */ package com.google.android.exoplayer2.text.eia608; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.style.BackgroundColorSpan; +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.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoder; @@ -24,14 +31,8 @@ import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; -import android.graphics.Color; -import android.graphics.Typeface; -import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; - import java.util.Collections; +import java.util.Iterator; import java.util.LinkedList; import java.util.TreeSet; @@ -472,10 +473,12 @@ public final class Eia608Decoder implements SubtitleDecoder { // from memory and from the display. The remaining rows of text are each rolled up into the // next highest row in the window, leaving the base row blank and ready to accept new text. if (captionMode == CC_MODE_ROLL_UP) { - for (Eia608CueBuilder cue : cues) { + Iterator iterator = cues.iterator(); + while (iterator.hasNext()) { + Eia608CueBuilder cue = iterator.next(); // Roll up all the other rows. if (!cue.rollUp()) { - cues.remove(cue); + iterator.remove(); } } currentCue = new Eia608CueBuilder(); From 98a5e199f9580a6a0d67096597e74388c064e587 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 14 Sep 2016 09:47:07 -0400 Subject: [PATCH 014/206] Corrected vertical positioning --- .../google/android/exoplayer2/text/eia608/Eia608CueBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index aee60cb38b..ffecf8b730 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -171,7 +171,7 @@ import java.util.List; public Cue build() { closeSpans(); - float cueLine = 10 + (5.33f * row); + float cueLine = 10 + (5.33f * (row - 1)); float cuePosition = 10 + (2.5f * indent); cuePosition = (tabOffset * 2.5f) + cuePosition; return new Cue(new SpannableStringBuilder(captionStringBuilder), From 6a3b66987a064119981c88c268c10a9ef5a19d85 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 14:52:38 +0100 Subject: [PATCH 015/206] Revert unrelated ContentDataSource change --- .../upstream/ContentDataSource.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 241296742a..f806f47410 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -142,13 +142,22 @@ public final class ContentDataSource implements DataSource { @Override public void close() throws ContentDataSourceException { uri = null; - if (inputStream != null) { - try { + try { + if (inputStream != null) { inputStream.close(); + } + } catch (IOException e) { + throw new ContentDataSourceException(e); + } finally { + inputStream = null; + try { + if (assetFileDescriptor != null) { + assetFileDescriptor.close(); + } } catch (IOException e) { throw new ContentDataSourceException(e); } finally { - inputStream = null; + assetFileDescriptor = null; if (opened) { opened = false; if (listener != null) { @@ -157,13 +166,6 @@ public final class ContentDataSource implements DataSource { } } } - - if (assetFileDescriptor != null) { - try { - assetFileDescriptor.close(); - } catch (Exception e) { - } - assetFileDescriptor = null; - } } + } From 3b34f850f25389d1a4fc240e883e872ede0308c0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 14:58:12 +0100 Subject: [PATCH 016/206] Clean up ID3 frame implementations --- .../exoplayer2/metadata/id3/ApicFrame.java | 53 +++++++++---------- .../exoplayer2/metadata/id3/BinaryFrame.java | 27 +++++----- .../exoplayer2/metadata/id3/CommentFrame.java | 34 +++++++----- .../exoplayer2/metadata/id3/GeobFrame.java | 51 ++++++++---------- .../exoplayer2/metadata/id3/Id3Decoder.java | 12 ++--- .../exoplayer2/metadata/id3/Id3Frame.java | 8 +-- .../exoplayer2/metadata/id3/PrivFrame.java | 47 ++++++++-------- .../metadata/id3/TextInformationFrame.java | 25 +++++---- .../exoplayer2/metadata/id3/TxxxFrame.java | 46 ++++++++-------- 9 files changed, 150 insertions(+), 153 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index 9acb6840a7..c64be24a31 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -39,8 +40,8 @@ public final class ApicFrame extends Id3Frame { this.pictureData = pictureData; } - public ApicFrame(Parcel in) { - super(in); + /* package */ ApicFrame(Parcel in) { + super(ID); mimeType = in.readString(); description = in.readString(); pictureType = in.readInt(); @@ -48,53 +49,49 @@ public final class ApicFrame extends Id3Frame { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ApicFrame that = (ApicFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (pictureType != that.pictureType) return false; - if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - if (description != null ? !description.equals(that.description) : that.description != null) - return false; - return Arrays.equals(pictureData, that.pictureData); + } + ApicFrame other = (ApicFrame) obj; + return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(description, other.description) + && Arrays.equals(pictureData, other.pictureData); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + pictureType; result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + pictureType; result = 31 * result + Arrays.hashCode(pictureData); return result; } @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(mimeType); dest.writeString(description); dest.writeInt(pictureType); dest.writeByteArray(pictureData); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public ApicFrame createFromParcel(Parcel in) { - return new ApicFrame(in); - } + @Override + public ApicFrame createFromParcel(Parcel in) { + return new ApicFrame(in); + } - @Override - public ApicFrame[] newArray(int size) { - return new ApicFrame[size]; - } + @Override + public ApicFrame[] newArray(int size) { + return new ApicFrame[size]; + } - }; + }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index a07bdf5934..f662c1d06f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -26,31 +26,32 @@ public final class BinaryFrame extends Id3Frame { public final byte[] data; - public BinaryFrame(String type, byte[] data) { - super(type); + public BinaryFrame(String id, byte[] data) { + super(id); this.data = data; } - public BinaryFrame(Parcel in) { - super(in); + /* package */ BinaryFrame(Parcel in) { + super(in.readString()); data = in.createByteArray(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - BinaryFrame that = (BinaryFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - return Arrays.equals(data, that.data); + } + BinaryFrame other = (BinaryFrame) obj; + return id.equals(other.id) && Arrays.equals(data, other.data); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + Arrays.hashCode(data); return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index 53b3b8212a..b7cc937ac4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -17,43 +17,51 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; /** * Comment ID3 frame. */ public final class CommentFrame extends Id3Frame { + public static final String ID = "COMM"; + public final String language; + public final String description; public final String text; public CommentFrame(String language, String description, String text) { - super(description); + super(ID); this.language = language; + this.description = description; this.text = text; } - public CommentFrame(Parcel in) { - super(in); + /* package */ CommentFrame(Parcel in) { + super(ID); language = in.readString(); + description = in.readString(); text = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - CommentFrame that = (CommentFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (language != null ? !language.equals(that.language) : that.language != null) return false; - return text != null ? text.equals(that.text) : that.text == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CommentFrame other = (CommentFrame) obj; + return Util.areEqual(description, other.description) && Util.areEqual(language, other.language) + && Util.areEqual(text, other.text); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (text != null ? text.hashCode() : 0); return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 5e4aa70b14..79e145fc7c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -39,8 +40,8 @@ public final class GeobFrame extends Id3Frame { this.data = data; } - public GeobFrame(Parcel in) { - super(in); + /* package */ GeobFrame(Parcel in) { + super(ID); mimeType = in.readString(); filename = in.readString(); description = in.readString(); @@ -48,25 +49,21 @@ public final class GeobFrame extends Id3Frame { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GeobFrame that = (GeobFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - if (filename != null ? !filename.equals(that.filename) : that.filename != null) - return false; - if (description != null ? !description.equals(that.description) : that.description != null) - return false; - return Arrays.equals(data, that.data); + } + GeobFrame other = (GeobFrame) obj; + return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename) + && Util.areEqual(description, other.description) && Arrays.equals(data, other.data); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + (filename != null ? filename.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); @@ -76,26 +73,24 @@ public final class GeobFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(mimeType); dest.writeString(filename); dest.writeString(description); dest.writeByteArray(data); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public GeobFrame createFromParcel(Parcel in) { - return new GeobFrame(in); - } + @Override + public GeobFrame createFromParcel(Parcel in) { + return new GeobFrame(in); + } - @Override - public GeobFrame[] newArray(int size) { - return new GeobFrame[size]; - } + @Override + public GeobFrame[] newArray(int size) { + return new GeobFrame[size]; + } - }; + }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 833162ab77..2c234a6042 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -345,8 +345,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, description); } - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, - int frameSize) throws UnsupportedEncodingException { + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -360,11 +360,11 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + int textStartIndex = descriptionEndIndex + delimiterLength(encoding); + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); - return new CommentFrame(language, description, value); + return new CommentFrame(language, description, text); } private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index ea4776d784..41c4ae4e03 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. @@ -29,11 +29,7 @@ public abstract class Id3Frame implements Parcelable { public final String id; public Id3Frame(String id) { - this.id = id; - } - - protected Id3Frame(Parcel in) { - id = in.readString(); + this.id = Assertions.checkNotNull(id); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index b0f9cb528f..fe55f5ddc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -35,27 +36,27 @@ public final class PrivFrame extends Id3Frame { this.privateData = privateData; } - public PrivFrame(Parcel in) { - super(in); + /* package */ PrivFrame(Parcel in) { + super(ID); owner = in.readString(); privateData = in.createByteArray(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PrivFrame that = (PrivFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false; - return Arrays.equals(privateData, that.privateData); + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PrivFrame other = (PrivFrame) obj; + return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (owner != null ? owner.hashCode() : 0); result = 31 * result + Arrays.hashCode(privateData); return result; @@ -63,24 +64,22 @@ public final class PrivFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(owner); dest.writeByteArray(privateData); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public PrivFrame createFromParcel(Parcel in) { - return new PrivFrame(in); - } + @Override + public PrivFrame createFromParcel(Parcel in) { + return new PrivFrame(in); + } - @Override - public PrivFrame[] newArray(int size) { - return new PrivFrame[size]; - } + @Override + public PrivFrame[] newArray(int size) { + return new PrivFrame[size]; + } - }; + }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 3c6409ca7d..b8c061fd0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; /** * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. @@ -30,25 +31,27 @@ public final class TextInformationFrame extends Id3Frame { this.description = description; } - public TextInformationFrame(Parcel in) { - super(in); + /* package */ TextInformationFrame(Parcel in) { + super(in.readString()); description = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TextInformationFrame that = (TextInformationFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - return description != null ? description.equals(that.description) : that.description == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TextInformationFrame other = (TextInformationFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java index 25ff1e063d..5c24e70ef4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import com.google.android.exoplayer2.util.Util; /** * TXXX (User defined text information) ID3 frame. @@ -34,28 +35,27 @@ public final class TxxxFrame extends Id3Frame { this.value = value; } - public TxxxFrame(Parcel in) { - super(in); + /* package */ TxxxFrame(Parcel in) { + super(ID); description = in.readString(); value = in.readString(); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TxxxFrame that = (TxxxFrame) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (description != null ? !description.equals(that.description) : that.description != null) + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; - return value != null ? value.equals(that.value) : that.value == null; + } + TxxxFrame other = (TxxxFrame) obj; + return Util.areEqual(description, other.description) && Util.areEqual(value, other.value); } @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = 17; result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (value != null ? value.hashCode() : 0); return result; @@ -63,24 +63,22 @@ public final class TxxxFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); dest.writeString(description); dest.writeString(value); } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public TxxxFrame createFromParcel(Parcel in) { - return new TxxxFrame(in); - } + @Override + public TxxxFrame createFromParcel(Parcel in) { + return new TxxxFrame(in); + } - @Override - public TxxxFrame[] newArray(int size) { - return new TxxxFrame[size]; - } + @Override + public TxxxFrame[] newArray(int size) { + return new TxxxFrame[size]; + } - }; + }; } From ba1da140c6e3bf6573688fc52c058f3bfe4b22b4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 16:27:58 +0100 Subject: [PATCH 017/206] Further modifications to ID3 support - Lots of misc cleanup - Remove GaplessInfo from Metadata. IMO it doesn't quite belong there, and means it ends up being represented twice inside Format. - Note: Changes untested, but will be tested in due course! --- .../android/exoplayer2/demo/EventLogger.java | 43 +++-- .../google/android/exoplayer2/FormatTest.java | 7 +- .../metadata/id3/Id3DecoderTest.java | 15 +- .../com/google/android/exoplayer2/Format.java | 13 +- .../android/exoplayer2/SimpleExoPlayer.java | 29 ++-- .../exoplayer2/extractor/GaplessInfo.java | 90 ----------- .../extractor/GaplessInfoHolder.java | 82 +++++++++- .../extractor/mp3/Mp3Extractor.java | 41 +++-- .../exoplayer2/extractor/mp4/AtomParsers.java | 152 ++++++++---------- .../extractor/mp4/Mp4Extractor.java | 19 +-- .../android/exoplayer2/metadata/Metadata.java | 117 ++++++++------ .../exoplayer2/metadata/MetadataBuilder.java | 42 ----- .../exoplayer2/metadata/MetadataDecoder.java | 6 +- .../exoplayer2/metadata/MetadataRenderer.java | 25 ++- .../exoplayer2/metadata/id3/Id3Decoder.java | 15 +- .../exoplayer2/metadata/id3/Id3Frame.java | 4 +- 16 files changed, 318 insertions(+), 382 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index f3c19da8d1..595f14e784 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; @@ -45,12 +44,10 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelections; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.text.NumberFormat; -import java.util.List; import java.util.Locale; /** @@ -59,7 +56,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output { + MappingTrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -179,44 +176,40 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output + // MetadataRenderer.Output @Override public void onMetadata(Metadata metadata) { - List id3Frames = metadata.getFrames(); - for (Id3Frame id3Frame : id3Frames) { - if (id3Frame instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) id3Frame; + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TxxxFrame) { + TxxxFrame txxxFrame = (TxxxFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); - } else if (id3Frame instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) id3Frame; + } else if (entry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); - } else if (id3Frame instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) id3Frame; + } else if (entry instanceof GeobFrame) { + GeobFrame geobFrame = (GeobFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (id3Frame instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) id3Frame; + } else if (entry instanceof ApicFrame) { + ApicFrame apicFrame = (ApicFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (id3Frame instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame; + } else if (entry instanceof TextInformationFrame) { + TextInformationFrame textInformationFrame = (TextInformationFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, textInformationFrame.description)); - } else if (id3Frame instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) id3Frame; + } else if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, commentFrame.language, commentFrame.text)); - } else { + } else if (entry instanceof Id3Frame) { + Id3Frame id3Frame = (Id3Frame) entry; Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); } } - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - if (gaplessInfo != null) { - Log.i(TAG, String.format("ID3 TimedMetadata encoder delay=%d padding=%d", - gaplessInfo.encoderDelay, gaplessInfo.encoderPadding)); - } } // AudioRendererEventListener diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index 9bdf330b02..1eb38de40f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -24,6 +24,8 @@ import android.annotation.TargetApi; import android.media.MediaFormat; import android.os.Parcel; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { TestUtil.buildTestData(128, 1 /* data seed */)); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); byte[] projectionData = new byte[] {1, 2, 3}; + Metadata metadata = new Metadata( + new TextInformationFrame("id1", "description1"), + new TextInformationFrame("id2", "description2")); Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, - drmInitData); + drmInitData, metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 97ebc6dbbc..8ec966967e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -32,9 +32,8 @@ public class Id3DecoderTest extends TestCase { 54, 52, 95, 115, 116, 97, 114, 116, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); assertEquals("", txxxFrame.description); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); } @@ -45,9 +44,8 @@ public class Id3DecoderTest extends TestCase { 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); assertEquals("image/jpeg", apicFrame.mimeType); assertEquals(16, apicFrame.pictureType); assertEquals("Hello World", apicFrame.description); @@ -60,9 +58,8 @@ public class Id3DecoderTest extends TestCase { 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - List id3Frames = metadata.getFrames(); - assertEquals(1, id3Frames.size()); - TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); + assertEquals(1, metadata.length()); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertEquals("TIT2", textInformationFrame.id); assertEquals("Hello World", textInformationFrame.description); } diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 078fbf98bd..65e797c8fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,7 +21,6 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -411,7 +410,7 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData, null); + language, subsampleOffsetUs, initializationData, drmInitData, metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -429,14 +428,10 @@ public final class Format implements Parcelable { } public Format copyWithMetadata(Metadata metadata) { - GaplessInfo gaplessInfo = metadata.getGaplessInfo(); - int ed = gaplessInfo != null ? gaplessInfo.encoderDelay : encoderDelay; - int ep = gaplessInfo != null ? gaplessInfo.encoderPadding : encoderPadding; - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, ed, ep, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, + stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5f43971de8..4829b44d25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private SurfaceHolder surfaceHolder; private TextureView textureView; private TextRenderer.Output textOutput; - private MetadataRenderer.Output id3Output; + private MetadataRenderer.Output metadataOutput; private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; @@ -389,12 +389,21 @@ public final class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive ID3 metadata events. + * @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead. + * @param output The output. + */ + @Deprecated + public void setId3Output(MetadataRenderer.Output output) { + setMetadataOutput(output); + } + + /** + * Sets a listener to receive metadata events. * * @param output The output. */ - public void setId3Output(MetadataRenderer.Output output) { - id3Output = output; + public void setMetadataOutput(MetadataRenderer.Output output) { + metadataOutput = output; } // ExoPlayer implementation @@ -539,9 +548,9 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); renderersList.add(textRenderer); - MetadataRenderer id3Renderer = new MetadataRenderer<>(componentListener, + MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener, mainHandler.getLooper(), new Id3Decoder()); - renderersList.add(id3Renderer); + renderersList.add(metadataRenderer); } private void buildExtensionRenderers(ArrayList renderersList, @@ -636,7 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, + AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback, TextureView.SurfaceTextureListener, TrackSelector.EventListener { @@ -768,12 +777,12 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - // MetadataRenderer.Output implementation + // MetadataRenderer.Output implementation @Override public void onMetadata(Metadata metadata) { - if (id3Output != null) { - id3Output.onMetadata(metadata); + if (metadataOutput != null) { + metadataOutput.onMetadata(metadata); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java deleted file mode 100644 index 7335d9103f..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2016 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.extractor; - -import android.util.Log; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Gapless playback information. - */ -public final class GaplessInfo { - - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; - private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); - - /** - * The number of samples to trim from the start of the decoded audio stream. - */ - public final int encoderDelay; - - /** - * The number of samples to trim from the end of the decoded audio stream. - */ - public final int encoderPadding; - - /** - * Parses gapless playback information from a gapless playback comment (stored in an ID3 header - * or MPEG 4 user data), if valid and non-zero. - * @param name The comment's identifier. - * @param data The comment's payload data. - * @return the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromComment(String name, String data) { - if(!GAPLESS_COMMENT_ID.equals(name)) { - return null; - } else { - Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); - if(matcher.find()) { - try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); - if(encoderDelay > 0 || encoderPadding > 0) { - Log.d("ExoplayerImpl", "Parsed gapless info: " + encoderDelay + " " + encoderPadding); - return new GaplessInfo(encoderDelay, encoderPadding); - } - } catch (NumberFormatException var5) { - ; - } - } - - // Ignore incorrectly formatted comments. - Log.d("ExoplayerImpl", "Unable to parse gapless info: " + data); - return null; - } - } - - /** - * Parses gapless playback information from an MP3 Xing header, if valid and non-zero. - * - * @param value The 24-bit value to decode. - * @return the gapless playback info, or null if the provided data is not valid. - */ - public static GaplessInfo createFromXingHeaderValue(int value) { - int encoderDelay = value >> 12; - int encoderPadding = value & 0x0FFF; - return encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : - null; - } - - public GaplessInfo(int encoderDelay, int encoderPadding) { - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 4f98ce4f7e..72d2e1abdf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -15,11 +15,91 @@ */ package com.google.android.exoplayer2.extractor; +import com.google.android.exoplayer2.Format; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Holder for gapless playback information. */ public final class GaplessInfoHolder { - public GaplessInfo gaplessInfo; + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final Pattern GAPLESS_COMMENT_PATTERN = + Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); + + /** + * The number of samples to trim from the start of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderPadding; + + /** + * Creates a new holder for gapless playback information. + */ + public GaplessInfoHolder() { + encoderDelay = Format.NO_VALUE; + encoderPadding = Format.NO_VALUE; + } + + /** + * Populates the holder with data from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return Whether the holder was populated. + */ + public boolean setFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + return false; + } + + /** + * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header + * or MPEG 4 user data), if valid and non-zero. + * + * @param name The comment's identifier. + * @param data The comment's payload data. + * @return Whether the holder was populated. + */ + public boolean setFromComment(String name, String data) { + if (!GAPLESS_COMMENT_ID.equals(name)) { + return false; + } + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if (matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + } catch (NumberFormatException e) { + // Ignore incorrectly formatted comments. + } + } + return false; + } + + /** + * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. + */ + public boolean hasGaplessInfo() { + return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 00f8e27ad2..a107b11b2e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -22,14 +22,18 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; +import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; + +import org.w3c.dom.Comment; + import java.io.EOFException; import java.io.IOException; @@ -70,7 +74,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private Metadata metadata; + private final GaplessInfoHolder gaplessInfoHolder; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -78,6 +82,7 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; + private Metadata metadata; private Seeker seeker; private long basisTimeUs; private long samplesRead; @@ -100,6 +105,7 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); + gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; } @@ -141,20 +147,13 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - - GaplessInfo gaplessInfo = metadata != null ? metadata.getGaplessInfo() : null; - Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderDelay : Format.NO_VALUE, - gaplessInfo != null ? gaplessInfo.encoderPadding : Format.NO_VALUE, - null, null, 0, null); - + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, + synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, null, null, 0, null); if (metadata != null) { format = format.copyWithMetadata(metadata); } - trackOutput.format(format); } return readSample(input); @@ -211,6 +210,17 @@ public final class Mp3Extractor implements Extractor { input.resetPeekPosition(); if (input.getPosition() == 0) { metadata = Id3Util.parseId3(input); + if (!gaplessInfoHolder.hasGaplessInfo()) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) { + break; + } + } + } + } peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -296,16 +306,13 @@ public final class Mp3Extractor implements Extractor { } if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); - if (seeker != null && metadata == null || metadata.getGaplessInfo() == null) { + if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); - GaplessInfo gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); - metadata = metadata != null ? - metadata.withGaplessInfo(gaplessInfo) : new Metadata(null, gaplessInfo); - + gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); } else if (frame.limit() >= 40) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 358c815098..05e20102fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -22,10 +22,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataBuilder; import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; @@ -38,6 +36,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.HevcConfig; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -95,8 +95,8 @@ import java.util.List; Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -286,7 +286,7 @@ import java.util.List; flags = rechunkedResults.flags; } - if (track.editListDurations == null || gaplessInfoHolder.gaplessInfo != null) { + if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); @@ -315,9 +315,10 @@ import java.util.List; track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale); - if ((encoderDelay > 0 || encoderPadding > 0) && encoderDelay <= Integer.MAX_VALUE + if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) { - gaplessInfoHolder.gaplessInfo = new GaplessInfo((int) encoderDelay, (int) encoderPadding); + gaplessInfoHolder.encoderDelay = (int) encoderDelay; + gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } @@ -402,13 +403,14 @@ import java.util.List; } /** - * Parses a udta atom for metadata, including gapless playback information. + * Parses a udta atom. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @return metadata stored in the user data, or {@code null} if not present. + * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ - public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, + GaplessInfoHolder out) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. @@ -422,14 +424,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + return parseMetaAtom(udtaData, out); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { + private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -438,9 +440,9 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - Metadata result = parseIlst(ilst); - if (result != null) { - return result; + Metadata metadata = parseIlst(ilst, out); + if (metadata != null) { + return metadata; } } data.skipBytes(payloadSize); @@ -448,19 +450,16 @@ import java.util.List; return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { - - MetadataBuilder builder = new MetadataBuilder(); - + private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + ArrayList entries = new ArrayList<>(); while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, builder); + parseIlstElement(ilst, type, endPosition, entries, out); ilst.setPosition(endPosition); } - - return builder.build(); + return entries.isEmpty() ? null : new Metadata(entries); } private static final String P1 = "\u00a9"; @@ -506,66 +505,64 @@ import java.util.List; // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - private static void parseIlstElement( - ParsableByteArray ilst, int type, int endPosition, MetadataBuilder builder) { + private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, + List builder, GaplessInfoHolder out) { if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst, endPosition); + parseTextAttribute(builder, "TIT2", ilst); } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst, endPosition); + parseCommentAttribute(builder, "COMM", ilst); } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst, endPosition); + parseTextAttribute(builder, "TDRC", ilst); } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst, endPosition); + parseTextAttribute(builder, "TPE1", ilst); } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst, endPosition); + parseTextAttribute(builder, "TSSE", ilst); } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst, endPosition); + parseTextAttribute(builder, "TALB", ilst); } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst, endPosition); + type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { + parseTextAttribute(builder, "TCOM", ilst); } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst, endPosition); + parseTextAttribute(builder, "lyrics", ilst); } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst, endPosition); + parseStandardGenreAttribute(builder, "TCON", ilst); } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst, endPosition); + parseTextAttribute(builder, "TCON", ilst); } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst, endPosition); + parseTextAttribute(builder, "TIT1", ilst); } else if (type == TYPE_DISK_NUMBER) { parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); } else if (type == TYPE_TRACK_NUMBER) { parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst, endPosition); + parseIntegerAttribute(builder, "TBPM", ilst); } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst, endPosition); + parseBooleanAttribute(builder, "TCMP", ilst); } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst, endPosition); + parseTextAttribute(builder, "TPE2", ilst); } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst, endPosition); + parseTextAttribute(builder, "TSOT", ilst); } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst, endPosition); + parseTextAttribute(builder, "TSO2", ilst); } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst, endPosition); + parseTextAttribute(builder, "TSOA", ilst); } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst, endPosition); + parseTextAttribute(builder, "TSOP", ilst); } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst, endPosition); + parseTextAttribute(builder, "TSOC", ilst); } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst, endPosition); + parseTextAttribute(builder, "sortShow", ilst); } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst, endPosition); + parseBooleanAttribute(builder, "gaplessAlbum", ilst); } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst, endPosition); + parseTextAttribute(builder, "show", ilst); } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); + parseExtendedAttribute(builder, ilst, endPosition, out); } } - private static void parseTextAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseTextAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -579,10 +576,8 @@ import java.util.List; } } - private static void parseCommentAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseCommentAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -596,10 +591,8 @@ import java.util.List; } } - private static void parseBooleanAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseBooleanAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -616,10 +609,8 @@ import java.util.List; } } - private static void parseIntegerAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIntegerAttribute(List builder, String attributeName, + ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -636,10 +627,8 @@ import java.util.List; } } - private static void parseIndexAndCountAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseIndexAndCountAttribute(List builder, + String attributeName, ParsableByteArray ilst, int endPosition) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -654,7 +643,7 @@ import java.util.List; String s = "" + index; if (count > 0) { s = s + "/" + count; - } + } Id3Frame frame = new TextInformationFrame(attributeName, s); builder.add(frame); } @@ -665,10 +654,8 @@ import java.util.List; } } - private static void parseStandardGenreAttribute(MetadataBuilder builder, - String attributeName, - ParsableByteArray ilst, - int endPosition) { + private static void parseStandardGenreAttribute(List builder, + String attributeName, ParsableByteArray ilst) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); @@ -690,9 +677,8 @@ import java.util.List; } } - private static void parseExtendedAttribute(MetadataBuilder builder, - ParsableByteArray ilst, - int endPosition) { + private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, + int endPosition, GaplessInfoHolder out) { String domain = null; String name = null; Object value = null; @@ -713,9 +699,9 @@ import java.util.List; } if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunSMPB")) { + if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) { String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); - builder.setGaplessInfo(GaplessInfo.createFromComment("iTunSMPB", s)); + out.setFromComment(name, s); } if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { @@ -889,12 +875,12 @@ import java.util.List; /** * Parses a stsd atom (defined in 14496-12). * - * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. - * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @param isQuickTime True for QuickTime media. False otherwise. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 1f4461f21e..303b4671cf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; @@ -311,16 +310,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; - GaplessInfo gaplessInfo = null; - Metadata metadata = null; + Metadata metadata = null; + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - Metadata info = AtomParsers.parseUdta(udta, isQuickTime); - if (info != null) { - gaplessInfo = info.getGaplessInfo(); - metadata = info; - } + metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); } for (int i = 0; i < moov.containerChildren.size(); i++) { @@ -337,10 +332,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); - gaplessInfoHolder.gaplessInfo = gaplessInfo; TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - gaplessInfo = gaplessInfoHolder.gaplessInfo; if (trackSampleTable.sampleCount == 0) { continue; } @@ -350,8 +342,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfo != null) { - format = format.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { + format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding); } if (metadata != null) { format = format.copyWithMetadata(metadata); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index c30e7ddb57..40c05a5602 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,65 +17,79 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** - * ID3 style metadata, with convenient access to gapless playback information. + * A collection of metadata entries. */ -public class Metadata implements Parcelable { +public final class Metadata implements Parcelable { - private final List frames; - private final GaplessInfo gaplessInfo; + /** + * A metadata entry. + */ + public interface Entry extends Parcelable {} - public Metadata(List frames, GaplessInfo gaplessInfo) { - List theFrames = frames != null ? new ArrayList<>(frames) : new ArrayList(); - this.frames = Collections.unmodifiableList(theFrames); - this.gaplessInfo = gaplessInfo; + private final Entry[] entries; + + /** + * @param entries The metadata entries. + */ + public Metadata(Entry... entries) { + this.entries = entries == null ? new Entry[0] : entries; } - public Metadata(Parcel in) { - int encoderDelay = in.readInt(); - int encoderPadding = in.readInt(); - gaplessInfo = encoderDelay > 0 || encoderPadding > 0 ? - new GaplessInfo(encoderDelay, encoderPadding) : null; - frames = Arrays.asList((Id3Frame[]) in.readArray(Id3Frame.class.getClassLoader())); + /** + * @param entries The metadata entries. + */ + public Metadata(List entries) { + if (entries != null) { + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); + } else { + this.entries = new Entry[0]; + } } - public Metadata withGaplessInfo(GaplessInfo info) { - return new Metadata(frames, info); + /* package */ Metadata(Parcel in) { + entries = new Metadata.Entry[in.readInt()]; + for (int i = 0; i < entries.length; i++) { + entries[i] = in.readParcelable(Entry.class.getClassLoader()); + } } - public List getFrames() { - return frames; + /** + * Returns the number of metadata entries. + */ + public int length() { + return entries.length; } - public GaplessInfo getGaplessInfo() { - return gaplessInfo; + /** + * Returns the entry at the specified index. + * + * @param index The index of the entry. + * @return The entry at the specified index. + */ + public Metadata.Entry get(int index) { + return entries[index]; } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Metadata that = (Metadata) o; - - if (!frames.equals(that.frames)) return false; - return gaplessInfo != null ? gaplessInfo.equals(that.gaplessInfo) : that.gaplessInfo == null; + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Metadata other = (Metadata) obj; + return Arrays.equals(entries, other.entries); } @Override public int hashCode() { - int result = frames.hashCode(); - result = 31 * result + (gaplessInfo != null ? gaplessInfo.hashCode() : 0); - return result; + return Arrays.hashCode(entries); } @Override @@ -85,21 +99,22 @@ public class Metadata implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderDelay : -1); - dest.writeInt(gaplessInfo != null ? gaplessInfo.encoderPadding : -1); - dest.writeArray(frames.toArray(new Id3Frame[frames.size()])); + dest.writeInt(entries.length); + for (Entry entry : entries) { + dest.writeParcelable(entry, 0); + } } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public Metadata createFromParcel(Parcel in) { - return new Metadata(in); - } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + + @Override + public Metadata[] newArray(int size) { + return new Metadata[0]; + } + }; - @Override - public Metadata[] newArray(int size) { - return new Metadata[0]; - } - }; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java deleted file mode 100644 index 57f49e5b20..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 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; - -import com.google.android.exoplayer2.extractor.GaplessInfo; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; - -import java.util.ArrayList; -import java.util.List; - -/** - * Builder for ID3 style metadata. - */ -public class MetadataBuilder { - private List frames = new ArrayList<>(); - private GaplessInfo gaplessInfo; - - public void add(Id3Frame frame) { - frames.add(frame); - } - - public void setGaplessInfo(GaplessInfo info) { - this.gaplessInfo = info; - } - - public Metadata build() { - return !frames.isEmpty() || gaplessInfo != null ? new Metadata(frames, gaplessInfo): null; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 7cde1f243d..a73311f16b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -17,10 +17,8 @@ package com.google.android.exoplayer2.metadata; /** * Decodes metadata from binary data. - * - * @param The type of the metadata. */ -public interface MetadataDecoder { +public interface MetadataDecoder { /** * Checks whether the decoder supports a given mime type. @@ -38,6 +36,6 @@ public interface MetadataDecoder { * @return The decoded metadata object. * @throws MetadataDecoderException If a problem occurred decoding the data. */ - T decode(byte[] data, int size) throws MetadataDecoderException; + Metadata decode(byte[] data, int size) throws MetadataDecoderException; } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index aca38a1258..ff1364610b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -30,38 +30,34 @@ import java.nio.ByteBuffer; /** * A renderer for metadata. - * - * @param The type of the metadata. */ -public final class MetadataRenderer extends BaseRenderer implements Callback { +public final class MetadataRenderer extends BaseRenderer implements Callback { /** * Receives output from a {@link MetadataRenderer}. - * - * @param The type of the metadata. */ - public interface Output { + public interface Output { /** * Called each time there is a metadata associated with current playback time. * * @param metadata The metadata. */ - void onMetadata(T metadata); + void onMetadata(Metadata metadata); } private static final int MSG_INVOKE_RENDERER = 0; - private final MetadataDecoder metadataDecoder; - private final Output output; + private final MetadataDecoder metadataDecoder; + private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; private final DecoderInputBuffer buffer; private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private T pendingMetadata; + private Metadata pendingMetadata; /** * @param output The output. @@ -72,8 +68,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback * called directly on the player's internal rendering thread. * @param metadataDecoder A decoder for the metadata. */ - public MetadataRenderer(Output output, Looper outputLooper, - MetadataDecoder metadataDecoder) { + public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); @@ -137,7 +132,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback return true; } - private void invokeRenderer(T metadata) { + private void invokeRenderer(Metadata metadata) { if (outputHandler != null) { outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -150,13 +145,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((T) msg.obj); + invokeRendererInternal((Metadata) msg.obj); return true; } return false; } - private void invokeRendererInternal(T metadata) { + private void invokeRendererInternal(Metadata metadata) { output.onMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 2c234a6042..8887af8675 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.GaplessInfo; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -31,7 +29,7 @@ import java.util.Locale; /** * Decodes individual TXXX text frames from raw ID3 data. */ -public final class Id3Decoder implements MetadataDecoder { +public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; @@ -41,7 +39,6 @@ public final class Id3Decoder implements MetadataDecoder { private int majorVersion; private int minorVersion; private boolean isUnsynchronized; - private GaplessInfo gaplessInfo; @Override public boolean canDecode(String mimeType) { @@ -141,11 +138,7 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeTextInformationFrame(frameData, frameSize, id); } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && (frameId3 == 'M' || frameId3 == 0)) { - CommentFrame commentFrame = decodeCommentFrame(frameData, frameSize); - frame = commentFrame; - if (gaplessInfo == null) { - gaplessInfo = GaplessInfo.createFromComment(commentFrame.id, commentFrame.text); - } + frame = decodeCommentFrame(frameData, frameSize); } else { String id = frameId3 != 0 ? String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : @@ -159,7 +152,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - return new Metadata(id3Frames, null); + return new Metadata(id3Frames); } private static int indexOfEos(byte[] data, int fromIndex, int encoding) { @@ -198,7 +191,7 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @return The size of ID3 frames in bytes, excluding the header and footer. - * @throws ParserException If ID3 file identifier != "ID3". + * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 41c4ae4e03..dc41d2250c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -16,12 +16,14 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcelable; + +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. */ -public abstract class Id3Frame implements Parcelable { +public abstract class Id3Frame implements Metadata.Entry { /** * The frame ID. From 97e7fb85a7eeec52f343c8e8f2934f82ff90e10c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 9 Oct 2016 18:00:59 +0100 Subject: [PATCH 018/206] ID3: Clean up logging + only add to audio track for MP4 --- .../android/exoplayer2/demo/EventLogger.java | 18 ++++++++++-------- .../exoplayer2/extractor/mp4/Mp4Extractor.java | 14 ++++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 595f14e784..74c777c4ee 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -180,36 +180,38 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { + Log.i(TAG, "metadata ["); for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id, + Log.i(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner)); + Log.i(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", + Log.i(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s", + Log.i(TAG, String.format(" %s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id, + Log.i(TAG, String.format(" %s: description=%s", textInformationFrame.id, textInformationFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s: language=%s text=%s", commentFrame.id, - commentFrame.language, commentFrame.text)); + Log.i(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id)); + Log.i(TAG, String.format(" %s", id3Frame.id)); } } + Log.i(TAG, "]"); } // AudioRendererEventListener diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 303b4671cf..6107a9ad75 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -342,12 +342,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); - } - if (metadata != null) { - format = format.copyWithMetadata(metadata); + if (track.type == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding); + } + if (metadata != null) { + format = format.copyWithMetadata(metadata); + } } mp4Track.trackOutput.format(format); From bffffb0fac093d7fb6934d266f48c59b4ecb6264 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 12 Oct 2016 17:27:54 +0100 Subject: [PATCH 019/206] Minor ID3 tweaks --- .../com/google/android/exoplayer2/Format.java | 15 +++++++-------- .../exoplayer2/extractor/GaplessInfoHolder.java | 1 - .../exoplayer2/extractor/mp3/Mp3Extractor.java | 11 ++--------- .../android/exoplayer2/metadata/id3/Id3Frame.java | 2 -- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 65e797c8fe..9528536296 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -58,6 +58,10 @@ public final class Format implements Parcelable { * Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ public final String codecs; + /** + * Metadata, or null if unknown or not applicable. + */ + public final Metadata metadata; // Container specific. @@ -87,11 +91,6 @@ public final class Format implements Parcelable { * DRM initialization data if the stream is protected, or null otherwise. */ public final DrmInitData drmInitData; - /** - * Static metadata - */ - public final Metadata metadata; - // Video specific. @@ -245,18 +244,18 @@ public final class Format implements Parcelable { @C.SelectionFlags int selectionFlags, String language) { return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, - selectionFlags, language); + selectionFlags, language, null); } public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { + @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, null); + initializationData, drmInitData, metadata); } // Text. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 72d2e1abdf..6eb9bc50de 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.Format; - import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index a107b11b2e..54c4219e5a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -31,9 +31,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - -import org.w3c.dom.Comment; - import java.io.EOFException; import java.io.IOException; @@ -147,14 +144,10 @@ public final class Mp3Extractor implements Extractor { if (seeker == null) { seeker = setupSeeker(input); extractorOutput.seekMap(seeker); - Format format = Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, + trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null); - if (metadata != null) { - format = format.copyWithMetadata(metadata); - } - trackOutput.format(format); + gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); } return readSample(input); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index dc41d2250c..9948f730eb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.os.Parcelable; - import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; From 50aeb20cc2f58ef6f5a0ad9ef52093b4be328ec3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 14:38:56 +0100 Subject: [PATCH 020/206] Make Id3Decoder stateless again One issue with the previous implementation was that isUnsynchronized would not be set back to false if previously set to true and if the next header has majorVersion >= 4. In general, having the decoder be stateless is clearer (and thread safe, albeit that this property is not required). --- .../exoplayer2/metadata/id3/Id3Decoder.java | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 8887af8675..a30adb2bdc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -36,10 +36,6 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private int majorVersion; - private int minorVersion; - private boolean isUnsynchronized; - @Override public boolean canDecode(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); @@ -49,20 +45,21 @@ public final class Id3Decoder implements MetadataDecoder { public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); - int id3Size = decodeId3Header(id3Data); + Id3Header id3Header = decodeId3Header(id3Data); - if (isUnsynchronized) { - id3Data = removeUnsynchronization(id3Data, id3Size); - id3Size = id3Data.bytesLeft(); + int framesBytesLeft = id3Header.framesSize; + if (id3Header.isUnsynchronized) { + id3Data = removeUnsynchronization(id3Data, id3Header.framesSize); + framesBytesLeft = id3Data.bytesLeft(); } - while (id3Size > 0) { + while (framesBytesLeft > 0) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = majorVersion > 2 ? id3Data.readUnsignedByte() : 0; - int frameSize = majorVersion == 2 ? id3Data.readUnsignedInt24() : - majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0; + int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() : + id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); if (frameSize <= 1) { break; @@ -75,9 +72,9 @@ public final class Id3Decoder implements MetadataDecoder { boolean hasGroupIdentifier = false; boolean hasDataLength = false; - if (majorVersion > 2) { + if (id3Header.majorVersion > 2) { int flags = id3Data.readShort(); - if (majorVersion == 3) { + if (id3Header.majorVersion == 3) { isCompressed = (flags & 0x0080) != 0; isEncrypted = (flags & 0x0040) != 0; hasDataLength = isCompressed; @@ -90,7 +87,7 @@ public final class Id3Decoder implements MetadataDecoder { } } - int headerSize = majorVersion == 2 ? 6 : 10; + int headerSize = id3Header.majorVersion == 2 ? 6 : 10; if (hasGroupIdentifier) { ++headerSize; @@ -110,7 +107,7 @@ public final class Id3Decoder implements MetadataDecoder { id3Data.skipBytes(4); } - id3Size -= frameSize + headerSize; + framesBytesLeft -= frameSize + headerSize; if (isCompressed || isEncrypted) { id3Data.skipBytes(frameSize); @@ -190,10 +187,11 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. - * @return The size of ID3 frames in bytes, excluding the header and footer. + * @return The parsed header. * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ - private int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { + private static Id3Header decodeId3Header(ParsableByteArray id3Buffer) + throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -202,11 +200,12 @@ public final class Id3Decoder implements MetadataDecoder { "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - majorVersion = id3Buffer.readUnsignedByte(); - minorVersion = id3Buffer.readUnsignedByte(); + int majorVersion = id3Buffer.readUnsignedByte(); + id3Buffer.skipBytes(1); // Skip minor version. + boolean isUnsynchronized = false; int flags = id3Buffer.readUnsignedByte(); - int id3Size = id3Buffer.readSynchSafeInt(); + int framesSize = id3Buffer.readSynchSafeInt(); if (majorVersion < 4) { // this flag is advisory in version 4, use the frame flags instead @@ -219,7 +218,7 @@ public final class Id3Decoder implements MetadataDecoder { int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { id3Buffer.skipBytes(extendedHeaderSize); - id3Size -= (extendedHeaderSize + 4); + framesSize -= (extendedHeaderSize + 4); } } } else if (majorVersion >= 4) { @@ -229,16 +228,16 @@ public final class Id3Decoder implements MetadataDecoder { if (extendedHeaderSize > 4) { id3Buffer.skipBytes(extendedHeaderSize - 4); } - id3Size -= extendedHeaderSize; + framesSize -= extendedHeaderSize; } // Check if footer presents. if ((flags & 0x10) != 0) { - id3Size -= 10; + framesSize -= 10; } } - return id3Size; + return new Id3Header(majorVersion, isUnsynchronized, framesSize); } private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) @@ -419,8 +418,25 @@ public final class Id3Decoder implements MetadataDecoder { } } - private final static String[] standardGenres = new String[] { + public static String decodeGenre(int code) { + return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; + } + private static final class Id3Header { + + private final int majorVersion; + private final boolean isUnsynchronized; + private final int framesSize; + + public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) { + this.majorVersion = majorVersion; + this.isUnsynchronized = isUnsynchronized; + this.framesSize = framesSize; + } + + } + + private static final String[] standardGenres = new String[] { // These are the official ID3v1 genres. "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", @@ -436,7 +452,6 @@ public final class Id3Decoder implements MetadataDecoder { "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", @@ -447,7 +462,6 @@ public final class Id3Decoder implements MetadataDecoder { "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", @@ -456,15 +470,4 @@ public final class Id3Decoder implements MetadataDecoder { "Synthpop" }; - public static String decodeGenre(int n) - { - n--; - - if (n < 0 || n >= standardGenres.length) { - return null; - } - - return standardGenres[n]; - } - } From 110c8f6f1f4459b05e9943fd6f5c8521735bbfbe Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:35:21 +0100 Subject: [PATCH 021/206] Improvements to ID3 decoder --- .../exoplayer2/metadata/id3/Id3Decoder.java | 394 +++++++++--------- 1 file changed, 202 insertions(+), 192 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index a30adb2bdc..8c6d449b07 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.util.Log; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -31,6 +32,8 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + private static final String TAG = "Id3Decoder"; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; @@ -45,201 +48,187 @@ public final class Id3Decoder implements MetadataDecoder { public Metadata decode(byte[] data, int size) throws MetadataDecoderException { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); - Id3Header id3Header = decodeId3Header(id3Data); - int framesBytesLeft = id3Header.framesSize; - if (id3Header.isUnsynchronized) { - id3Data = removeUnsynchronization(id3Data, id3Header.framesSize); - framesBytesLeft = id3Data.bytesLeft(); + Id3Header id3Header = decodeHeader(id3Data); + if (id3Header == null) { + return null; } - while (framesBytesLeft > 0) { - int frameId0 = id3Data.readUnsignedByte(); - int frameId1 = id3Data.readUnsignedByte(); - int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Header.majorVersion > 2 ? id3Data.readUnsignedByte() : 0; - int frameSize = id3Header.majorVersion == 2 ? id3Data.readUnsignedInt24() : - id3Header.majorVersion == 3 ? id3Data.readInt() : id3Data.readSynchSafeInt(); + int startPosition = id3Data.getPosition(); + int framesSize = id3Header.framesSize; + if (id3Header.isUnsynchronized) { + framesSize = removeUnsynchronization(id3Data, id3Header.framesSize); + } + id3Data.setLimit(startPosition + framesSize); - if (frameSize <= 1) { - break; - } - - // Frame flags. - boolean isCompressed = false; - boolean isEncrypted = false; - boolean isUnsynchronized = false; - boolean hasGroupIdentifier = false; - boolean hasDataLength = false; - - if (id3Header.majorVersion > 2) { - int flags = id3Data.readShort(); - if (id3Header.majorVersion == 3) { - isCompressed = (flags & 0x0080) != 0; - isEncrypted = (flags & 0x0040) != 0; - hasDataLength = isCompressed; - } else { - isCompressed = (flags & 0x0008) != 0; - isEncrypted = (flags & 0x0004) != 0; - isUnsynchronized = (flags & 0x0002) != 0; - hasGroupIdentifier = (flags & 0x0040) != 0; - hasDataLength = (flags & 0x0001) != 0; - } - } - - int headerSize = id3Header.majorVersion == 2 ? 6 : 10; - - if (hasGroupIdentifier) { - ++headerSize; - --frameSize; - id3Data.skipBytes(1); - } - - if (isEncrypted) { - ++headerSize; - --frameSize; - id3Data.skipBytes(1); - } - - if (hasDataLength) { - headerSize += 4; - frameSize -= 4; - id3Data.skipBytes(4); - } - - framesBytesLeft -= frameSize + headerSize; - - if (isCompressed || isEncrypted) { - id3Data.skipBytes(frameSize); - } else { - try { - Id3Frame frame; - ParsableByteArray frameData = id3Data; - if (isUnsynchronized) { - frameData = removeUnsynchronization(id3Data, frameSize); - frameSize = frameData.bytesLeft(); - } - - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - frame = decodeTxxxFrame(frameData, frameSize); - } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { - frame = decodePrivFrame(frameData, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { - frame = decodeGeobFrame(frameData, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(frameData, frameSize); - } else if (frameId0 == 'T') { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); - frame = decodeTextInformationFrame(frameData, frameSize, id); - } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && - (frameId3 == 'M' || frameId3 == 0)) { - frame = decodeCommentFrame(frameData, frameSize); - } else { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); - frame = decodeBinaryFrame(frameData, frameSize, id); - } - id3Frames.add(frame); - } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported character encoding"); - } + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; + while (id3Data.bytesLeft() >= frameHeaderSize) { + Id3Frame frame = decodeFrame(id3Header, id3Data); + if (frame != null) { + id3Frames.add(frame); } } return new Metadata(id3Frames); } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { - int terminationPos = indexOfZeroByte(data, fromIndex); - - // For single byte encoding charsets, we're done. - if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { - return terminationPos; - } - - // Otherwise ensure an even index and look for a second zero byte. - while (terminationPos < data.length - 1) { - if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { - return terminationPos; - } - terminationPos = indexOfZeroByte(data, terminationPos + 1); - } - - return data.length; - } - - private static int indexOfZeroByte(byte[] data, int fromIndex) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] == (byte) 0) { - return i; - } - } - return data.length; - } - - private static int delimiterLength(int encodingByte) { - return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) - ? 1 : 2; - } - /** - * @param id3Buffer A {@link ParsableByteArray} from which data should be read. - * @return The parsed header. - * @throws MetadataDecoderException If ID3 file identifier != "ID3". + * @param data A {@link ParsableByteArray} from which the header should be read. + * @return The parsed header, or null if the ID3 tag is unsupported. + * @throws MetadataDecoderException If the first three bytes differ from "ID3". */ - private static Id3Header decodeId3Header(ParsableByteArray id3Buffer) + private static Id3Header decodeHeader(ParsableByteArray data) throws MetadataDecoderException { - int id1 = id3Buffer.readUnsignedByte(); - int id2 = id3Buffer.readUnsignedByte(); - int id3 = id3Buffer.readUnsignedByte(); + int id1 = data.readUnsignedByte(); + int id2 = data.readUnsignedByte(); + int id3 = data.readUnsignedByte(); if (id1 != 'I' || id2 != 'D' || id3 != '3') { throw new MetadataDecoderException(String.format(Locale.US, - "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + "Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - int majorVersion = id3Buffer.readUnsignedByte(); - id3Buffer.skipBytes(1); // Skip minor version. - boolean isUnsynchronized = false; + int majorVersion = data.readUnsignedByte(); + data.skipBytes(1); // Skip minor version. + int flags = data.readUnsignedByte(); + int framesSize = data.readSynchSafeInt(); - int flags = id3Buffer.readUnsignedByte(); - int framesSize = id3Buffer.readSynchSafeInt(); - - if (majorVersion < 4) { - // this flag is advisory in version 4, use the frame flags instead - isUnsynchronized = (flags & 0x80) != 0; - } - - if (majorVersion == 3) { - // check for extended header - if ((flags & 0x40) != 0) { - int extendedHeaderSize = id3Buffer.readInt(); // size excluding size field - if (extendedHeaderSize == 6 || extendedHeaderSize == 10) { - id3Buffer.skipBytes(extendedHeaderSize); - framesSize -= (extendedHeaderSize + 4); - } + if (majorVersion == 2) { + boolean isCompressed = (flags & 0x40) != 0; + if (isCompressed) { + Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme"); + return null; } - } else if (majorVersion >= 4) { - // check for extended header - if ((flags & 0x40) != 0) { - int extendedHeaderSize = id3Buffer.readSynchSafeInt(); // size including size field - if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); - } + } else if (majorVersion == 3) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readInt(); // Size excluding size field. + data.skipBytes(extendedHeaderSize); + framesSize -= (extendedHeaderSize + 4); + } + } else if (majorVersion == 4) { + boolean hasExtendedHeader = (flags & 0x40) != 0; + if (hasExtendedHeader) { + int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field. + data.skipBytes(extendedHeaderSize - 4); framesSize -= extendedHeaderSize; } - - // Check if footer presents. - if ((flags & 0x10) != 0) { + boolean hasFooter = (flags & 0x10) != 0; + if (hasFooter) { framesSize -= 10; } + } else { + Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion); + return null; } + // isUnsynchronized is advisory only in version 4. Frame level flags are used instead. + boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0; return new Id3Header(majorVersion, isUnsynchronized, framesSize); } + private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data) + throws MetadataDecoderException { + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; + + int frameSize; + if (id3Header.majorVersion == 4) { + frameSize = id3Data.readUnsignedIntToInt(); + if ((frameSize & 0x808080L) == 0) { + // Parse the frame size as a syncsafe integer, as per the spec. + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } else { + // Proceed using the frame size read as an unsigned integer. + Log.w(TAG, "Frame size not specified as syncsafe integer"); + } + } else if (id3Header.majorVersion == 3) { + frameSize = id3Data.readUnsignedIntToInt(); + } else /* id3Header.majorVersion == 2 */ { + frameSize = id3Data.readUnsignedInt24(); + } + + int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0; + if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 + && flags == 0) { + // We must be reading zero padding at the end of the tag. + id3Data.setPosition(id3Data.limit()); + return null; + } + + int nextFramePosition = id3Data.getPosition() + frameSize; + + // Frame flags. + boolean isCompressed = false; + boolean isEncrypted = false; + boolean isUnsynchronized = false; + boolean hasDataLength = false; + boolean hasGroupIdentifier = false; + if (id3Header.majorVersion == 3) { + isCompressed = (flags & 0x0080) != 0; + isEncrypted = (flags & 0x0040) != 0; + hasGroupIdentifier = (flags & 0x0020) != 0; + hasDataLength = isCompressed; + } else if (id3Header.majorVersion == 4) { + hasGroupIdentifier = (flags & 0x0040) != 0; + isCompressed = (flags & 0x0008) != 0; + isEncrypted = (flags & 0x0004) != 0; + isUnsynchronized = (flags & 0x0002) != 0; + hasDataLength = (flags & 0x0001) != 0; + } + + if (isCompressed || isEncrypted) { + Log.w(TAG, "Skipping unsupported compressed or encrypted frame"); + id3Data.setPosition(nextFramePosition); + return null; + } + + if (hasGroupIdentifier) { + frameSize--; + id3Data.skipBytes(1); + } + if (hasDataLength) { + frameSize -= 4; + id3Data.skipBytes(4); + } + if (isUnsynchronized) { + frameSize = removeUnsynchronization(id3Data, frameSize); + } + + try { + Id3Frame frame; + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + frame = decodeTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = decodePrivFrame(id3Data, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + frame = decodeGeobFrame(id3Data, frameSize); + } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { + frame = decodeApicFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeTextInformationFrame(id3Data, frameSize, id); + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && + (frameId3 == 'M' || frameId3 == 0)) { + frame = decodeCommentFrame(id3Data, frameSize); + } else { + String id = frameId3 != 0 ? + String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : + String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + frame = decodeBinaryFrame(id3Data, frameSize, id); + } + return frame; + } catch (UnsupportedEncodingException e) { + throw new MetadataDecoderException("Unsupported character encoding"); + } finally { + id3Data.setPosition(nextFramePosition); + } + } + private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); @@ -368,34 +357,22 @@ public final class Id3Decoder implements MetadataDecoder { } /** - * Undo the unsynchronization applied to one or more frames. - * @param dataSource The original data, positioned at the beginning of a frame. - * @param count The number of valid bytes in the frames to be processed. - * @return replacement data for the frames. + * Performs in-place removal of unsynchronization for {@code length} bytes starting from + * {@link ParsableByteArray#getPosition()} + * + * @param data Contains the data to be processed. + * @param length The length of the data to be processed. + * @return The length of the data after processing. */ - private static ParsableByteArray removeUnsynchronization(ParsableByteArray dataSource, int count) { - byte[] source = dataSource.data; - int sourceIndex = dataSource.getPosition(); - int limit = sourceIndex + count; - byte[] dest = new byte[count]; - int destIndex = 0; - - while (sourceIndex < limit) { - byte b = source[sourceIndex++]; - if ((b & 0xFF) == 0xFF) { - int nextIndex = sourceIndex+1; - if (nextIndex < limit) { - int b2 = source[nextIndex]; - if (b2 == 0) { - // skip the 0 byte - ++sourceIndex; - } - } + private static int removeUnsynchronization(ParsableByteArray data, int length) { + byte[] bytes = data.data; + for (int i = data.getPosition(); i + 1 < length; i++) { + if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { + System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2); + length--; } - dest[destIndex++] = b; } - - return new ParsableByteArray(dest, destIndex); + return length; } /** @@ -418,6 +395,39 @@ public final class Id3Decoder implements MetadataDecoder { } } + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + public static String decodeGenre(int code) { return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; } From 4391014a7af98b227377b0f4bf75cac245914f33 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:45:09 +0100 Subject: [PATCH 022/206] Split genres into separate util class --- .../exoplayer2/extractor/mp4/AtomParsers.java | 5 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 38 ----------- .../exoplayer2/metadata/id3/Id3Util.java | 63 +++++++++++++++++++ 3 files changed, 65 insertions(+), 41 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index f829c7b4ee..d91d677f87 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.id3.Id3Util; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; import com.google.android.exoplayer2.video.HevcConfig; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -666,7 +665,7 @@ import java.util.List; byte[] bytes = (byte[]) value; if (bytes.length == 2) { int code = (bytes[0] << 8) + (bytes[1] & 0xFF); - String s = Id3Decoder.decodeGenre(code); + String s = Id3Util.decodeGenre(code); if (s != null) { Id3Frame frame = new TextInformationFrame(attributeName, s); builder.add(frame); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 8c6d449b07..46b7dbde76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -428,10 +428,6 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } - public static String decodeGenre(int code) { - return (0 < code && code <= standardGenres.length) ? standardGenres[code - 1] : null; - } - private static final class Id3Header { private final int majorVersion; @@ -446,38 +442,4 @@ public final class Id3Decoder implements MetadataDecoder { } - private static final String[] standardGenres = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", - "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", - "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", - "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", - "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", - "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", - "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", - "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", - "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", - "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", - "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", - "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", - "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", - "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", - "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" - }; - } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java new file mode 100644 index 0000000000..9a7a6e7cb8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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; + +/** + * ID3 utility methods. + */ +public class Id3Util { + + private static final String[] STANDARD_GENRES = new String[] { + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + private Id3Util() {} + + public static String decodeGenre(int code) { + return (0 < code && code <= STANDARD_GENRES.length) ? STANDARD_GENRES[code - 1] : null; + } + +} From 66652f65bbad83df8b6bef20a7d24943e7f6a17a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 17 Oct 2016 22:47:03 +0100 Subject: [PATCH 023/206] Make Id3Util final --- .../com/google/android/exoplayer2/metadata/id3/Id3Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java index 9a7a6e7cb8..64f2ce9908 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.metadata.id3; /** * ID3 utility methods. */ -public class Id3Util { +public final class Id3Util { private static final String[] STANDARD_GENRES = new String[] { // These are the official ID3v1 genres. From 7594f5b78ba216f6085675972f830b2ebfe230ee Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Oct 2016 15:02:35 +0100 Subject: [PATCH 024/206] Further enhance ID3 decoder + support --- .../extractor/GaplessInfoHolder.java | 21 +++ .../exoplayer2/extractor/mp3/Id3Util.java | 95 ---------- .../extractor/mp3/Mp3Extractor.java | 78 +++++++-- .../exoplayer2/metadata/id3/Id3Decoder.java | 162 ++++++++++-------- 4 files changed, 175 insertions(+), 181 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 6eb9bc50de..4b5fa977ee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -65,6 +67,25 @@ public final class GaplessInfoHolder { return false; } + /** + * Populates the holder with data parsed from ID3 {@link Metadata}. + * + * @param metadata The metadata from which to parse the gapless information. + * @return Whether the holder was populated. + */ + public boolean setFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (setFromComment(commentFrame.description, commentFrame.text)) { + return true; + } + } + } + return false; + } + /** * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * or MPEG 4 user data), if valid and non-zero. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java deleted file mode 100644 index af08514889..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Id3Util.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2016 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.extractor.mp3; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; -import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; - -/** - * Utility for parsing ID3 version 2 metadata in MP3 files. - */ -/* package */ final class Id3Util { - - /** - * The maximum valid length for metadata in bytes. - */ - private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; - - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - - /** - * Peeks data from the input and parses ID3 metadata, including gapless playback information. - * - * @param input The {@link ExtractorInput} from which data should be peeked. - * @return The metadata, if present, {@code null} otherwise. - * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. - */ - public static Metadata parseId3(ExtractorInput input) - throws IOException, InterruptedException { - Metadata result = null; - ParsableByteArray scratch = new ParsableByteArray(10); - int peekedId3Bytes = 0; - while (true) { - input.peekFully(scratch.data, 0, 10); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { - break; - } - - int majorVersion = scratch.readUnsignedByte(); - int minorVersion = scratch.readUnsignedByte(); - int flags = scratch.readUnsignedByte(); - int length = scratch.readSynchSafeInt(); - int frameLength = length + 10; - - try { - if (canParseMetadata(majorVersion, minorVersion, flags, length)) { - input.resetPeekPosition(); - byte[] frame = new byte[frameLength]; - input.peekFully(frame, 0, frameLength); - return new Id3Decoder().decode(frame, frameLength); - } else { - input.advancePeekPosition(length); - } - } catch (MetadataDecoderException e) { - e.printStackTrace(); - } - - peekedId3Bytes += frameLength; - } - input.resetPeekPosition(); - input.advancePeekPosition(peekedId3Bytes); - return result; - } - - private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, - int length) { - return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4 - && length <= MAXIMUM_METADATA_SIZE - && !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0)) - && !(majorVersion == 3 && (flags & 0x1F) != 0) - && !(majorVersion == 4 && (flags & 0x0F) != 0); - } - - private Id3Util() {} - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 54c4219e5a..acec0c5567 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -28,7 +29,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -51,6 +53,8 @@ public final class Mp3Extractor implements Extractor { }; + private static final String TAG = "Mp3Extractor"; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -59,6 +63,18 @@ public final class Mp3Extractor implements Extractor { * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + /** + * First three bytes of a well formed ID3 tag header. + */ + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** + * Length of an ID3 tag header. + */ + private static final int ID3_HEADER_LENGTH = 10; + /** + * Maximum length of data read into {@link #scratch}. + */ + private static final int SCRATCH_LENGTH = 10; /** * Mask that includes the audio header values that must match between frames. @@ -100,7 +116,7 @@ public final class Mp3Extractor implements Extractor { */ public Mp3Extractor(long forcedFirstSampleTimestampUs) { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; - scratch = new ParsableByteArray(4); + scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; @@ -147,7 +163,7 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, null)); } return readSample(input); } @@ -202,18 +218,7 @@ public final class Mp3Extractor implements Extractor { int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; input.resetPeekPosition(); if (input.getPosition() == 0) { - metadata = Id3Util.parseId3(input); - if (!gaplessInfoHolder.hasGaplessInfo()) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - if (gaplessInfoHolder.setFromComment(commentFrame.description, commentFrame.text)) { - break; - } - } - } - } + peekId3Data(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -267,6 +272,49 @@ public final class Mp3Extractor implements Extractor { return true; } + /** + * Peeks ID3 data from the input, including gapless playback information. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { + int peekedId3Bytes = 0; + while (true) { + input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + // Not an ID3 tag. + break; + } + scratch.skipBytes(3); // Skip major version, minor version and flags. + int framesLength = scratch.readSynchSafeInt(); + int tagLength = ID3_HEADER_LENGTH + framesLength; + + try { + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH); + input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength); + metadata = new Id3Decoder().decode(id3Data, tagLength); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } + } else { + input.advancePeekPosition(framesLength); + } + } catch (MetadataDecoderException e) { + Log.e(TAG, "Failed to decode ID3 tag", e); + } + + peekedId3Bytes += tagLength; + } + + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + } + /** * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide * data from the start of the first frame in the stream. On returning, the input's position will diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 46b7dbde76..3e1bbe159a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -63,7 +63,7 @@ public final class Id3Decoder implements MetadataDecoder { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header, id3Data); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data); if (frame != null) { id3Frames.add(frame); } @@ -72,6 +72,40 @@ public final class Id3Decoder implements MetadataDecoder { return new Metadata(id3Frames); } + // TODO: Move the following three methods nearer to the bottom of the file. + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + /** * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. @@ -126,15 +160,15 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private Id3Frame decodeFrame(Id3Header id3Header, ParsableByteArray id3Data) + private Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data) throws MetadataDecoderException { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); - int frameId3 = id3Header.majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; + int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0; int frameSize; - if (id3Header.majorVersion == 4) { + if (majorVersion == 4) { frameSize = id3Data.readUnsignedIntToInt(); if ((frameSize & 0x808080L) == 0) { // Parse the frame size as a syncsafe integer, as per the spec. @@ -144,13 +178,13 @@ public final class Id3Decoder implements MetadataDecoder { // Proceed using the frame size read as an unsigned integer. Log.w(TAG, "Frame size not specified as syncsafe integer"); } - } else if (id3Header.majorVersion == 3) { + } else if (majorVersion == 3) { frameSize = id3Data.readUnsignedIntToInt(); } else /* id3Header.majorVersion == 2 */ { frameSize = id3Data.readUnsignedInt24(); } - int flags = id3Header.majorVersion >= 2 ? id3Data.readShort() : 0; + int flags = majorVersion >= 3 ? id3Data.readShort() : 0; if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 && flags == 0) { // We must be reading zero padding at the end of the tag. @@ -159,6 +193,9 @@ public final class Id3Decoder implements MetadataDecoder { } int nextFramePosition = id3Data.getPosition() + frameSize; + if (nextFramePosition > id3Data.limit()) { + return null; + } // Frame flags. boolean isCompressed = false; @@ -166,12 +203,12 @@ public final class Id3Decoder implements MetadataDecoder { boolean isUnsynchronized = false; boolean hasDataLength = false; boolean hasGroupIdentifier = false; - if (id3Header.majorVersion == 3) { + if (majorVersion == 3) { isCompressed = (flags & 0x0080) != 0; isEncrypted = (flags & 0x0040) != 0; hasGroupIdentifier = (flags & 0x0020) != 0; hasDataLength = isCompressed; - } else if (id3Header.majorVersion == 4) { + } else if (majorVersion == 4) { hasGroupIdentifier = (flags & 0x0040) != 0; isCompressed = (flags & 0x0008) != 0; isEncrypted = (flags & 0x0004) != 0; @@ -199,26 +236,29 @@ public final class Id3Decoder implements MetadataDecoder { try { Id3Frame frame; - if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeTxxxFrame(id3Data, frameSize); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { frame = decodePrivFrame(id3Data, frameSize); - } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' + && (frameId3 == 'B' || majorVersion == 2)) { frame = decodeGeobFrame(id3Data, frameSize); - } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { - frame = decodeApicFrame(id3Data, frameSize); + } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C') + : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) { + frame = decodeApicFrame(id3Data, frameSize, majorVersion); } else if (frameId0 == 'T') { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + String id = majorVersion == 2 + ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) + : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); frame = decodeTextInformationFrame(id3Data, frameSize, id); - } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && - (frameId3 == 'M' || frameId3 == 0)) { + } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' + && (frameId3 == 'M' || majorVersion == 2)) { frame = decodeCommentFrame(id3Data, frameSize); } else { - String id = frameId3 != 0 ? - String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3) : - String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2); + String id = majorVersion == 2 + ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) + : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); frame = decodeBinaryFrame(id3Data, frameSize, id); } return frame; @@ -288,16 +328,29 @@ public final class Id3Decoder implements MetadataDecoder { return new GeobFrame(mimeType, filename, description, objectData); } - private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int mimeTypeEndIndex = indexOfZeroByte(data, 0); - String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + String mimeType; + int mimeTypeEndIndex; + if (majorVersion == 2) { + mimeTypeEndIndex = 2; + mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase(); + if (mimeType.equals("image/jpg")) { + mimeType = "image/jpeg"; + } + } else { + mimeTypeEndIndex = indexOfZeroByte(data, 0); + mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase(); + if (mimeType.indexOf('/') == -1) { + mimeType = "image/" + mimeType; + } + } int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; @@ -312,20 +365,6 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) throws UnsupportedEncodingException { - int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); - - byte[] data = new byte[frameSize - 1]; - id3Data.readBytes(data, 0, frameSize - 1); - - int descriptionEndIndex = indexOfEos(data, 0, encoding); - String description = new String(data, 0, descriptionEndIndex, charset); - - return new TextInformationFrame(id, description); - } - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); @@ -348,6 +387,20 @@ public final class Id3Decoder implements MetadataDecoder { return new CommentFrame(language, description, text); } + private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, + int frameSize, String id) throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + return new TextInformationFrame(id, description); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; @@ -395,39 +448,6 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { - int terminationPos = indexOfZeroByte(data, fromIndex); - - // For single byte encoding charsets, we're done. - if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { - return terminationPos; - } - - // Otherwise ensure an even index and look for a second zero byte. - while (terminationPos < data.length - 1) { - if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { - return terminationPos; - } - terminationPos = indexOfZeroByte(data, terminationPos + 1); - } - - return data.length; - } - - private static int indexOfZeroByte(byte[] data, int fromIndex) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] == (byte) 0) { - return i; - } - } - return data.length; - } - - private static int delimiterLength(int encodingByte) { - return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) - ? 1 : 2; - } - private static final class Id3Header { private final int majorVersion; From 7e352295d7f966b6e5b2b7520bd766840be8423d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Oct 2016 15:17:01 +0100 Subject: [PATCH 025/206] Propagate ID3 data for MP3 --- .../android/exoplayer2/demo/EventLogger.java | 79 +++++++++++-------- .../metadata/id3/Id3DecoderTest.java | 3 +- .../extractor/mp3/Mp3Extractor.java | 2 +- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index bde8aa6220..c3fc5b9549 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -40,10 +40,10 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; @@ -56,7 +56,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - MappingTrackSelector.EventListener, MetadataRenderer.Output { + TrackSelector.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -183,38 +183,9 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { - Log.i(TAG, "metadata ["); - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.i(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, - txxxFrame.description, txxxFrame.value)); - } else if (entry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) entry; - Log.i(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); - } else if (entry instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) entry; - Log.i(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (entry instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) entry; - Log.i(TAG, String.format(" %s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.i(TAG, String.format(" %s: description=%s", textInformationFrame.id, - textInformationFrame.description)); - } else if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - Log.i(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); - } else if (entry instanceof Id3Frame) { - Id3Frame id3Frame = (Id3Frame) entry; - Log.i(TAG, String.format(" %s", id3Frame.id)); - } - } - Log.i(TAG, "]"); + Log.d(TAG, "onMetadata ["); + printMetadata(metadata); + Log.d(TAG, "]"); } // AudioRendererEventListener @@ -237,8 +208,13 @@ import java.util.Locale; @Override public void onAudioInputFormatChanged(Format format) { + boolean hasMetadata = format.metadata != null; Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) - + "]"); + + (hasMetadata ? "" : "]")); + if (hasMetadata) { + printMetadata(format.metadata); + Log.d(TAG, "]"); + } } @Override @@ -359,6 +335,39 @@ import java.util.Locale; Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } + private void printMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TxxxFrame) { + TxxxFrame txxxFrame = (TxxxFrame) entry; + Log.d(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, + txxxFrame.description, txxxFrame.value)); + } else if (entry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) entry; + Log.d(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); + } else if (entry instanceof GeobFrame) { + GeobFrame geobFrame = (GeobFrame) entry; + Log.d(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", + geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); + } else if (entry instanceof ApicFrame) { + ApicFrame apicFrame = (ApicFrame) entry; + Log.d(TAG, String.format(" %s: mimeType=%s, description=%s", + apicFrame.id, apicFrame.mimeType, apicFrame.description)); + } else if (entry instanceof TextInformationFrame) { + TextInformationFrame textInformationFrame = (TextInformationFrame) entry; + Log.d(TAG, String.format(" %s: description=%s", textInformationFrame.id, + textInformationFrame.description)); + } else if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + Log.d(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description)); + } else if (entry instanceof Id3Frame) { + Id3Frame id3Frame = (Id3Frame) entry; + Log.d(TAG, String.format(" %s", id3Frame.id)); + } + } + } + private String getSessionTimeString() { return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 8ec966967e..6bfa6fccfc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -16,9 +16,8 @@ package com.google.android.exoplayer2.metadata.id3; import android.test.MoreAsserts; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.Metadata; -import java.util.List; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; import junit.framework.TestCase; /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index acec0c5567..9d3a7c541f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -163,7 +163,7 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, null)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); } return readSample(input); } From 06fb29c939e74f7198a12bcece7b668df05ee52f Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 17 Oct 2016 08:13:33 -0700 Subject: [PATCH 026/206] Support caching of multi segment DASH, HLS and Smooth Streaming. Use sha1 of content uri if a custom cache key or content id isn't provided. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136351830 --- .../upstream/cache/CacheDataSourceTest.java | 3 +- .../source/ExtractorMediaPeriod.java | 4 +- .../source/dash/DashMediaSource.java | 6 +-- .../source/dash/manifest/Representation.java | 38 +++++++++++-------- .../upstream/cache/CacheDataSource.java | 26 ++++--------- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 5e85ad4d4c..d46458db2b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -177,8 +177,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { builder.setSimulateUnknownLength(simulateUnknownLength); builder.appendReadData(TEST_DATA); FakeDataSource upstream = builder.build(); - return new CacheDataSource(simpleCache, upstream, - CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_CACHE_UNBOUNDED_REQUESTS, + return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE, MAX_CACHE_FILE_SIZE); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 27bd1f677f..18de1b9df9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ConditionVariable; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -593,8 +592,7 @@ import java.io.IOException; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open( - new DataSpec(uri, position, C.LENGTH_UNSET, Util.sha1(uri.toString()))); + length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, null)); if (length != C.LENGTH_UNSET) { length += position; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 766f1e0ebf..f22d1693a9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -124,7 +124,7 @@ public final class DashMediaSource implements MediaSource { this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - manifestParser = new DashManifestParser(generateContentId()); + manifestParser = new DashManifestParser(); manifestCallback = new ManifestCallback(); manifestUriLock = new Object(); periodsById = new SparseArray<>(); @@ -468,10 +468,6 @@ public final class DashMediaSource implements MediaSource { } } - private String generateContentId() { - return Util.sha1(manifestUri.toString()); - } - private static final class PeriodSeekInfo { public static PeriodSeekInfo createPeriodSeekInfo( diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 9c6d2e1582..6ebd69e29b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -57,7 +57,6 @@ public abstract class Representation { */ public final long presentationTimeOffsetUs; - private final String cacheKey; private final RangedUri initializationUri; /** @@ -81,7 +80,8 @@ public abstract class Representation { * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param segmentBase A segment base element for the representation. - * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. + * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This + * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, @@ -91,7 +91,7 @@ public abstract class Representation { (SingleSegmentBase) segmentBase, customCacheKey, C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation(contentId, revisionId, format, - (MultiSegmentBase) segmentBase, customCacheKey); + (MultiSegmentBase) segmentBase); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -99,12 +99,10 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, - SegmentBase segmentBase, String customCacheKey) { + SegmentBase segmentBase) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; - this.cacheKey = customCacheKey != null ? customCacheKey - : contentId + "." + format.id + "." + revisionId; initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } @@ -129,12 +127,10 @@ public abstract class Representation { public abstract DashSegmentIndex getIndex(); /** - * Returns a cache key for the representation, in the format - * {@code contentId + "." + format.id + "." + revisionId}. + * Returns a cache key for the representation if a custom cache key or content id has been + * provided and there is only single segment. */ - public String getCacheKey() { - return cacheKey; - } + public abstract String getCacheKey(); /** * A DASH representation consisting of a single segment. @@ -151,6 +147,7 @@ public abstract class Representation { */ public final long contentLength; + private final String cacheKey; private final RangedUri indexUri; private final SingleSegmentIndex segmentIndex; @@ -187,9 +184,11 @@ public abstract class Representation { */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, SingleSegmentBase segmentBase, String customCacheKey, long contentLength) { - super(contentId, revisionId, format, segmentBase, customCacheKey); + super(contentId, revisionId, format, segmentBase); this.uri = Uri.parse(segmentBase.uri); this.indexUri = segmentBase.getIndex(); + this.cacheKey = customCacheKey != null ? customCacheKey + : contentId != null ? contentId + "." + format.id + "." + revisionId : null; this.contentLength = contentLength; // If we have an index uri then the index is defined externally, and we shouldn't return one // directly. If we don't, then we can't do better than an index defining a single segment. @@ -207,6 +206,11 @@ public abstract class Representation { return segmentIndex; } + @Override + public String getCacheKey() { + return cacheKey; + } + } /** @@ -222,11 +226,10 @@ public abstract class Representation { * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param segmentBase The segment base underlying the representation. - * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - MultiSegmentBase segmentBase, String customCacheKey) { - super(contentId, revisionId, format, segmentBase, customCacheKey); + MultiSegmentBase segmentBase) { + super(contentId, revisionId, format, segmentBase); this.segmentBase = segmentBase; } @@ -240,6 +243,11 @@ public abstract class Representation { return this; } + @Override + public String getCacheKey() { + return null; + } + // DashSegmentIndex implementation. @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 727eb068ce..1f56d4ef83 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; @@ -50,8 +51,7 @@ public final class CacheDataSource implements DataSource { * Flags controlling the cache's behavior. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR, - FLAG_CACHE_UNBOUNDED_REQUESTS}) + @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR}) public @interface Flags {} /** * A flag indicating whether we will block reads if the cache key is locked. If this flag is @@ -66,13 +66,6 @@ public final class CacheDataSource implements DataSource { */ public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; - /** - * A flag indicating whether the response is cached if the range of the request is unbounded. - * Disabled by default because, as a side effect, this may allow streams with every chunk from a - * separate URL cached which is broken currently. - */ - public static final int FLAG_CACHE_UNBOUNDED_REQUESTS = 1 << 2; - /** * Listener of {@link CacheDataSource} events. */ @@ -98,7 +91,6 @@ public final class CacheDataSource implements DataSource { private final boolean blockOnCache; private final boolean ignoreCacheOnError; - private final boolean bypassUnboundedRequests; private DataSource currentDataSource; private boolean currentRequestUnbounded; @@ -127,8 +119,8 @@ public final class CacheDataSource implements DataSource { * * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} - * and {@link #FLAG_CACHE_UNBOUNDED_REQUESTS} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link + * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size * exceeds this value, then the data will be fragmented into multiple cache files. The * finer-grained this is the finer-grained the eviction policy can be. @@ -148,8 +140,8 @@ public final class CacheDataSource implements DataSource { * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} - * and {@link #FLAG_CACHE_UNBOUNDED_REQUESTS} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link + * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, @@ -158,7 +150,6 @@ public final class CacheDataSource implements DataSource { this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0; - this.bypassUnboundedRequests = (flags & FLAG_CACHE_UNBOUNDED_REQUESTS) == 0; this.upstreamDataSource = upstream; if (cacheWriteDataSink != null) { this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink); @@ -173,10 +164,9 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key; + key = dataSpec.key != null ? dataSpec.key : Util.sha1(uri.toString()); readPosition = dataSpec.position; - currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) - || (bypassUnboundedRequests && dataSpec.length == C.LENGTH_UNSET); + currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError; if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { bytesRemaining = dataSpec.length; } else { From d3f78e48081d71b75bedeeeee870972a7f9a24a5 Mon Sep 17 00:00:00 2001 From: kapishnikov Date: Mon, 17 Oct 2016 14:54:17 -0700 Subject: [PATCH 027/206] Deprecate call to new UrlRequest.Builder(...) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136399474 --- .../ext/cronet/CronetDataSourceTest.java | 60 ++++--------------- .../ext/cronet/CronetDataSource.java | 4 +- 2 files changed, 14 insertions(+), 50 deletions(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index b0de0784de..c214503f0c 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -52,7 +51,6 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -88,20 +86,7 @@ public final class CronetDataSourceTest { private Map testResponseHeader; private UrlResponseInfo testUrlResponseInfo; - /** - * MockableCronetEngine is an abstract class for helping creating new Requests. - */ - public abstract static class MockableCronetEngine extends CronetEngine { - - @Override - public abstract UrlRequest createRequest(String url, UrlRequest.Callback callback, - Executor executor, int priority, - Collection connectionAnnotations, - boolean disableCache, - boolean disableConnectionMigration, - boolean allowDirectExecutor); - } - + @Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private UrlRequest mockUrlRequest; @Mock @@ -114,8 +99,7 @@ public final class CronetDataSourceTest { private Executor mockExecutor; @Mock private UrlRequestException mockUrlRequestException; - @Mock - private MockableCronetEngine mockCronetEngine; + @Mock private CronetEngine mockCronetEngine; private CronetDataSource dataSourceUnderTest; @@ -135,15 +119,10 @@ public final class CronetDataSourceTest { true, // resetTimeoutOnRedirects mockClock)); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); - when(mockCronetEngine.createRequest( - anyString(), - any(UrlRequest.Callback.class), - any(Executor.class), - anyInt(), - eq(Collections.emptyList()), - any(Boolean.class), - any(Boolean.class), - any(Boolean.class))).thenReturn(mockUrlRequest); + when(mockCronetEngine.newUrlRequestBuilder( + anyString(), any(UrlRequest.Callback.class), any(Executor.class))) + .thenReturn(mockUrlRequestBuilder); + when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest); mockStatusResponse(); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); @@ -184,15 +163,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.close(); // Prepare a mock UrlRequest to be used in the second open() call. final UrlRequest mockUrlRequest2 = mock(UrlRequest.class); - when(mockCronetEngine.createRequest( - anyString(), - any(UrlRequest.Callback.class), - any(Executor.class), - anyInt(), - eq(Collections.emptyList()), - any(Boolean.class), - any(Boolean.class), - any(Boolean.class))).thenReturn(mockUrlRequest2); + when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -215,15 +186,8 @@ public final class CronetDataSourceTest { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); - verify(mockCronetEngine).createRequest( - eq(TEST_URL), - any(UrlRequest.Callback.class), - any(Executor.class), - anyInt(), - eq(Collections.emptyList()), - any(Boolean.class), - any(Boolean.class), - any(Boolean.class)); + verify(mockCronetEngine) + .newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class)); verify(mockUrlRequest).start(); } @@ -237,9 +201,9 @@ public final class CronetDataSourceTest { dataSourceUnderTest.open(testDataSpec); // The header value to add is current position to current position + length - 1. - verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999"); - verify(mockUrlRequest).addHeader("firstHeader", "firstValue"); - verify(mockUrlRequest).addHeader("secondHeader", "secondValue"); + verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999"); + verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue"); + verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue"); verify(mockUrlRequest).start(); } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 0190668a70..83f46bd488 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -412,8 +412,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou // Internal methods. private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException { - UrlRequest.Builder requestBuilder = new UrlRequest.Builder(dataSpec.uri.toString(), this, - executor, cronetEngine); + UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), + this, executor); // Set the headers. synchronized (requestProperties) { if (dataSpec.postBody != null && dataSpec.postBody.length != 0 From 1cfc432bb8a9db2feaf72b226a6cc0efceb881a6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 18 Oct 2016 11:53:00 -0700 Subject: [PATCH 028/206] Add REQUIRE_HTTPS flag Note that it's not possible for the library to enforce that the flag is adhered to, since it's possible for applications to inject custom implementations of DataSource (there's no requirement they even extend HttpDataSource for network requesting implementations). It's possible for applications to replace pretty much anything in the library, so there's no other place we could put the flag where we could make this guarantee. Hence this is a best-effort that will work when using HttpDataSource implementations that we provide. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136500947 --- .../ext/cronet/CronetDataSource.java | 3 +- .../ext/okhttp/OkHttpDataSource.java | 3 ++ .../android/exoplayer2/ExoPlayerFlags.java | 32 +++++++++++++++++++ .../android/exoplayer2/upstream/DataSpec.java | 7 ++++ .../upstream/DefaultHttpDataSource.java | 3 ++ 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 83f46bd488..e75524c1c9 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -20,6 +20,7 @@ import android.os.ConditionVariable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -206,7 +207,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { - Assertions.checkNotNull(dataSpec); + Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); Assertions.checkState(!opened); operation.close(); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 2b6eaa736d..07c607771a 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -145,6 +146,8 @@ public class OkHttpDataSource implements HttpDataSource { @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { + Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); + this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java new file mode 100644 index 0000000000..0af9208d59 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 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; + +/** + * Global configuration flags. Applications may toggle these flags, but should do so prior to any + * other use of the library. + */ +public final class ExoPlayerFlags { + + /** + * If set, indicates to {@link com.google.android.exoplayer2.upstream.HttpDataSource} + * implementations that they should reject non-HTTPS requests. + */ + public static boolean REQUIRE_HTTPS = false; + + private ExoPlayerFlags() {} + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index d251446976..ced276d7eb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -167,6 +167,13 @@ public final class DataSpec { this.flags = flags; } + /** + * Returns whether the instance defines a HTTPS request. + */ + public boolean isHttps() { + return uri != null && "https".equalsIgnoreCase(uri.getScheme()); + } + @Override public String toString() { return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index b326c41b18..2b3f91c77c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; @@ -186,6 +187,8 @@ public class DefaultHttpDataSource implements HttpDataSource { @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { + Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); + this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; From aecbbdd36c669f1c98facac91b92eb07850bf38c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 18 Oct 2016 11:55:47 -0700 Subject: [PATCH 029/206] Replace IndexOutOfBounds Exception for BehindLiveWindowException This is a problem when two invocations of getNextChunk that retrieve chunks (i.e. not playlists) occur too apart in time. In that case the last loaded chunk has a media sequence that is behind the current playlist. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136501291 --- .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 53d9e70d76..a23ab3bae7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -440,6 +440,10 @@ import java.util.Locale; } HlsMediaPlaylist oldMediaPlaylist = variantPlaylists[oldVariantIndex]; HlsMediaPlaylist newMediaPlaylist = variantPlaylists[newVariantIndex]; + if (previousChunkIndex < oldMediaPlaylist.mediaSequence) { + // We have fallen behind the live window. + return newMediaPlaylist.mediaSequence - 1; + } double offsetToLiveInstantSecs = 0; for (int i = previousChunkIndex - oldMediaPlaylist.mediaSequence; i < oldMediaPlaylist.segments.size(); i++) { From aa1002a0d616b2ce50ce52d2d18e5c223a1365cd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Oct 2016 05:16:36 -0700 Subject: [PATCH 030/206] Rollback "Add REQUIRE_HTTPS flag" *** Reason for rollback *** Flag doesn't enforce what it says it enforces, due to redirects *** Original change description *** Add REQUIRE_HTTPS flag Note that it's not possible for the library to enforce that the flag is adhered to, since it's possible for applications to inject custom implementations of DataSource (there's no requirement they even extend HttpDataSource for network requesting implementations). It's possible for applications to replace pretty much anything in the library, so there's no other place we could put the flag where we could make this guarantee. Hence this is a best-effort that will work when... *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136583459 --- .../ext/cronet/CronetDataSource.java | 3 +- .../ext/okhttp/OkHttpDataSource.java | 3 -- .../android/exoplayer2/ExoPlayerFlags.java | 32 ------------------- .../android/exoplayer2/upstream/DataSpec.java | 7 ---- .../upstream/DefaultHttpDataSource.java | 3 -- 5 files changed, 1 insertion(+), 47 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index e75524c1c9..83f46bd488 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -20,7 +20,6 @@ import android.os.ConditionVariable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -207,7 +206,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { - Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); + Assertions.checkNotNull(dataSpec); Assertions.checkState(!opened); operation.close(); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 07c607771a..2b6eaa736d 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -146,8 +145,6 @@ public class OkHttpDataSource implements HttpDataSource { @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { - Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); - this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java deleted file mode 100644 index 0af9208d59..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFlags.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2016 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; - -/** - * Global configuration flags. Applications may toggle these flags, but should do so prior to any - * other use of the library. - */ -public final class ExoPlayerFlags { - - /** - * If set, indicates to {@link com.google.android.exoplayer2.upstream.HttpDataSource} - * implementations that they should reject non-HTTPS requests. - */ - public static boolean REQUIRE_HTTPS = false; - - private ExoPlayerFlags() {} - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ced276d7eb..d251446976 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -167,13 +167,6 @@ public final class DataSpec { this.flags = flags; } - /** - * Returns whether the instance defines a HTTPS request. - */ - public boolean isHttps() { - return uri != null && "https".equalsIgnoreCase(uri.getScheme()); - } - @Override public String toString() { return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 2b3f91c77c..b326c41b18 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -19,7 +19,6 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFlags; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; @@ -187,8 +186,6 @@ public class DefaultHttpDataSource implements HttpDataSource { @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { - Assertions.checkState(!ExoPlayerFlags.REQUIRE_HTTPS || dataSpec.isHttps()); - this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; From 3a5cb435412d6acce4407e7b6fae170b006cb00e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Oct 2016 07:59:35 -0700 Subject: [PATCH 031/206] Fix use of API level 19 method Issue: #1965 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136595233 --- .../android/exoplayer2/ui/PlaybackControlView.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 3823f1760e..096e67ec01 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -75,6 +75,7 @@ public class PlaybackControlView extends FrameLayout { private ExoPlayer player; private VisibilityListener visibilityListener; + private boolean isAttachedToWindow; private boolean dragging; private int rewindMs; private int fastForwardMs; @@ -264,7 +265,7 @@ public class PlaybackControlView extends FrameLayout { removeCallbacks(hideAction); if (showTimeoutMs > 0) { hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; - if (isAttachedToWindow()) { + if (isAttachedToWindow) { postDelayed(hideAction, showTimeoutMs); } } else { @@ -279,7 +280,7 @@ public class PlaybackControlView extends FrameLayout { } private void updatePlayPauseButton() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } boolean playing = player != null && player.getPlayWhenReady(); @@ -291,7 +292,7 @@ public class PlaybackControlView extends FrameLayout { } private void updateNavigation() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; @@ -315,7 +316,7 @@ public class PlaybackControlView extends FrameLayout { } private void updateProgress() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } long duration = player == null ? 0 : player.getDuration(); @@ -426,6 +427,7 @@ public class PlaybackControlView extends FrameLayout { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + isAttachedToWindow = true; if (hideAtMs != C.TIME_UNSET) { long delayMs = hideAtMs - SystemClock.uptimeMillis(); if (delayMs <= 0) { @@ -440,6 +442,7 @@ public class PlaybackControlView extends FrameLayout { @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); + isAttachedToWindow = false; removeCallbacks(updateProgressAction); removeCallbacks(hideAction); } From 586e6257cd009d137e1fd9250f45036d3f06145a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Oct 2016 08:18:48 -0700 Subject: [PATCH 032/206] Add explicit TargetApi annotation to remove lint error ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136597149 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 096e67ec01..89c778d072 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.SystemClock; @@ -351,13 +352,18 @@ public class PlaybackControlView extends FrameLayout { private void setButtonEnabled(boolean enabled, View view) { view.setEnabled(enabled); if (Util.SDK_INT >= 11) { - view.setAlpha(enabled ? 1f : 0.3f); + setViewAlphaV11(view, enabled ? 1f : 0.3f); view.setVisibility(VISIBLE); } else { view.setVisibility(enabled ? VISIBLE : INVISIBLE); } } + @TargetApi(11) + private void setViewAlphaV11(View view, float alpha) { + view.setAlpha(alpha); + } + private String stringForTime(long timeMs) { if (timeMs == C.TIME_UNSET) { timeMs = 0; From 3e3248d712cffabd203c5547cdfff80b636feb69 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Oct 2016 17:18:17 +0100 Subject: [PATCH 033/206] Yet more misc ID3 improvements --- .../extractor/mp3/Mp3Extractor.java | 40 ++----- .../exoplayer2/metadata/id3/Id3Decoder.java | 108 ++++++++++++++---- 2 files changed, 97 insertions(+), 51 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 9d3a7c541f..02b92f2077 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -29,7 +28,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -53,8 +51,6 @@ public final class Mp3Extractor implements Extractor { }; - private static final String TAG = "Mp3Extractor"; - /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -63,14 +59,6 @@ public final class Mp3Extractor implements Extractor { * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; - /** - * First three bytes of a well formed ID3 tag header. - */ - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - /** - * Length of an ID3 tag header. - */ - private static final int ID3_HEADER_LENGTH = 10; /** * Maximum length of data read into {@link #scratch}. */ @@ -282,30 +270,26 @@ public final class Mp3Extractor implements Extractor { private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { int peekedId3Bytes = 0; while (true) { - input.peekFully(scratch.data, 0, ID3_HEADER_LENGTH); + input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { + if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { // Not an ID3 tag. break; } scratch.skipBytes(3); // Skip major version, minor version and flags. int framesLength = scratch.readSynchSafeInt(); - int tagLength = ID3_HEADER_LENGTH + framesLength; + int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; - try { - if (metadata == null) { - byte[] id3Data = new byte[tagLength]; - System.arraycopy(scratch.data, 0, id3Data, 0, ID3_HEADER_LENGTH); - input.peekFully(id3Data, ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); - } - } else { - input.advancePeekPosition(framesLength); + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); + metadata = new Id3Decoder().decode(id3Data, tagLength); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); } - } catch (MetadataDecoderException e) { - Log.e(TAG, "Failed to decode ID3 tag", e); + } else { + input.advancePeekPosition(framesLength); } peekedId3Bytes += tagLength; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 3e1bbe159a..05bff672a4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.metadata.id3; import android.util.Log; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -28,12 +28,21 @@ import java.util.List; import java.util.Locale; /** - * Decodes individual TXXX text frames from raw ID3 data. + * Decodes ID3 tags. */ public final class Id3Decoder implements MetadataDecoder { private static final String TAG = "Id3Decoder"; + /** + * The first three bytes of a well formed ID3 tag header. + */ + public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** + * Length of an ID3 tag header. + */ + public static final int ID3_HEADER_LENGTH = 10; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; @@ -45,7 +54,7 @@ public final class Id3Decoder implements MetadataDecoder { } @Override - public Metadata decode(byte[] data, int size) throws MetadataDecoderException { + public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -61,9 +70,21 @@ public final class Id3Decoder implements MetadataDecoder { } id3Data.setLimit(startPosition + framesSize); + boolean unsignedIntFrameSizeHack = false; + if (id3Header.majorVersion == 4) { + if (!validateV4Frames(id3Data, false)) { + if (validateV4Frames(id3Data, true)) { + unsignedIntFrameSizeHack = true; + } else { + Log.w(TAG, "Failed to validate V4 ID3 tag"); + return null; + } + } + } + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack); if (frame != null) { id3Frames.add(frame); } @@ -109,16 +130,17 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. - * @throws MetadataDecoderException If the first three bytes differ from "ID3". */ - private static Id3Header decodeHeader(ParsableByteArray data) - throws MetadataDecoderException { - int id1 = data.readUnsignedByte(); - int id2 = data.readUnsignedByte(); - int id3 = data.readUnsignedByte(); - if (id1 != 'I' || id2 != 'D' || id3 != '3') { - throw new MetadataDecoderException(String.format(Locale.US, - "Unexpected ID3 tag identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + private static Id3Header decodeHeader(ParsableByteArray data) { + if (data.bytesLeft() < ID3_HEADER_LENGTH) { + Log.w(TAG, "Data too short to be an ID3 tag"); + return null; + } + + int id = data.readUnsignedInt24(); + if (id != ID3_TAG) { + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id); + return null; } int majorVersion = data.readUnsignedByte(); @@ -129,7 +151,7 @@ public final class Id3Decoder implements MetadataDecoder { if (majorVersion == 2) { boolean isCompressed = (flags & 0x40) != 0; if (isCompressed) { - Log.w(TAG, "Skipped ID3 tag with majorVersion=1 and undefined compression scheme"); + Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme"); return null; } } else if (majorVersion == 3) { @@ -160,8 +182,49 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data) - throws MetadataDecoderException { + private static boolean validateV4Frames(ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack) { + int startPosition = id3Data.getPosition(); + try { + while (id3Data.bytesLeft() >= 10) { + int id = id3Data.readInt(); + int frameSize = id3Data.readUnsignedIntToInt(); + int flags = id3Data.readUnsignedShort(); + if (id == 0 && frameSize == 0 && flags == 0) { + return true; + } else { + if (!unsignedIntFrameSizeHack) { + // Parse the data size as a synchsafe integer, as per the spec. + if ((frameSize & 0x808080L) != 0) { + return false; + } + frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) + | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); + } + int minimumFrameSize = 0; + if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) { + minimumFrameSize++; + } + if ((flags & 0x0001) != 0 /* hasDataLength */) { + minimumFrameSize += 4; + } + if (frameSize < minimumFrameSize) { + return false; + } + if (id3Data.bytesLeft() < frameSize) { + return false; + } + id3Data.skipBytes(frameSize); // flags + } + } + return true; + } finally { + id3Data.setPosition(startPosition); + } + } + + private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -170,13 +233,9 @@ public final class Id3Decoder implements MetadataDecoder { int frameSize; if (majorVersion == 4) { frameSize = id3Data.readUnsignedIntToInt(); - if ((frameSize & 0x808080L) == 0) { - // Parse the frame size as a syncsafe integer, as per the spec. + if (!unsignedIntFrameSizeHack) { frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); - } else { - // Proceed using the frame size read as an unsigned integer. - Log.w(TAG, "Frame size not specified as syncsafe integer"); } } else if (majorVersion == 3) { frameSize = id3Data.readUnsignedIntToInt(); @@ -184,7 +243,7 @@ public final class Id3Decoder implements MetadataDecoder { frameSize = id3Data.readUnsignedInt24(); } - int flags = majorVersion >= 3 ? id3Data.readShort() : 0; + int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0; if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0 && flags == 0) { // We must be reading zero padding at the end of the tag. @@ -194,6 +253,8 @@ public final class Id3Decoder implements MetadataDecoder { int nextFramePosition = id3Data.getPosition() + frameSize; if (nextFramePosition > id3Data.limit()) { + Log.w(TAG, "Frame size exceeds remaining tag data"); + id3Data.setPosition(id3Data.limit()); return null; } @@ -263,7 +324,8 @@ public final class Id3Decoder implements MetadataDecoder { } return frame; } catch (UnsupportedEncodingException e) { - throw new MetadataDecoderException("Unsupported character encoding"); + Log.w(TAG, "Unsupported character encoding"); + return null; } finally { id3Data.setPosition(nextFramePosition); } From aa660acf0300d0ac22f54bd3ba80ceab77f4b21e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Oct 2016 12:00:35 +0100 Subject: [PATCH 034/206] Fix typo in comments --- .../android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 2b6eaa736d..e56072f368 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -65,7 +65,7 @@ public class OkHttpDataSource implements HttpDataSource { private long bytesRead; /** - * @param callFactory An {@link Call.Factory} for use by the source. + * @param callFactory A {@link Call.Factory} for use by the source. * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. @@ -76,7 +76,7 @@ public class OkHttpDataSource implements HttpDataSource { } /** - * @param callFactory An {@link Call.Factory} for use by the source. + * @param callFactory A {@link Call.Factory} for use by the source. * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from From bebbf29a786f98f5ffd37f072c9b44974ae60e5d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 19 Oct 2016 09:58:41 -0700 Subject: [PATCH 035/206] Fix NPE when trying to play H265 in Ts files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136607848 --- .../google/android/exoplayer2/extractor/ts/H265Reader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 6283371a19..57d7e77bb7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,6 +44,7 @@ import java.util.Collections; private static final int SUFFIX_SEI_NUT = 40; private TrackOutput output; + private SampleReader sampleReader; private SeiReader seiReader; // State that should not be reset on seek. @@ -56,7 +57,6 @@ import java.util.Collections; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer prefixSei; private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? - private final SampleReader sampleReader; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -72,7 +72,6 @@ import java.util.Collections; pps = new NalUnitTargetBuffer(PPS_NUT, 128); prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); - sampleReader = new SampleReader(output); seiWrapper = new ParsableByteArray(); } @@ -91,6 +90,7 @@ import java.util.Collections; @Override public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); + sampleReader = new SampleReader(output); seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); } From f56b05f0d73f6ff0b646002c2a8d0acfd21fe723 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Oct 2016 03:35:32 -0700 Subject: [PATCH 036/206] Bump version to r2.0.4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136697697 --- RELEASENOTES.md | 21 ++++++++++++------- build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- playbacktests/src/main/AndroidManifest.xml | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e0439dd12..ce002238ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,22 +1,27 @@ # Release notes # +### r2.0.4 ### + +This release contains important bug fixes. Users of earlier r2.0.x versions +should proactively update to this version. + +* Fix crash on Jellybean devices when using playback controls + ([#1965](https://github.com/google/ExoPlayer/issues/1965)). + ### r2.0.3 ### -This release contains important bug fixes. Users of r2.0.0, r2.0.1 and r2.0.2 -should proactively update to this version. - * Fixed NullPointerException in ExtractorMediaSource - ([#1914](https://github.com/google/ExoPlayer/issues/1914). + ([#1914](https://github.com/google/ExoPlayer/issues/1914)). * Fixed NullPointerException in HlsMediaPeriod - ([#1907](https://github.com/google/ExoPlayer/issues/1907). + ([#1907](https://github.com/google/ExoPlayer/issues/1907)). * Fixed memory leak in PlaybackControlView - ([#1908](https://github.com/google/ExoPlayer/issues/1908). + ([#1908](https://github.com/google/ExoPlayer/issues/1908)). * Fixed strict mode violation when using SimpleExoPlayer.setVideoPlayerTextureView(). * Fixed L3 Widevine provisioning - ([#1925](https://github.com/google/ExoPlayer/issues/1925). + ([#1925](https://github.com/google/ExoPlayer/issues/1925)). * Fixed hiding of controls with use_controller="false" - ([#1919](https://github.com/google/ExoPlayer/issues/1919). + ([#1919](https://github.com/google/ExoPlayer/issues/1919)). * Improvements to Cronet network stack extension. * Misc bug fixes. diff --git a/build.gradle b/build.gradle index c50dd31b27..8e9032be70 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.0.3' + releaseVersion = 'r2.0.4' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 7fc0ac3d9c..1f015827c9 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2004" + android:versionName="2.0.4"> diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 23e6d4d593..02c70bb0be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.0.3"; + String VERSION = "2.0.4"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2000003; + int VERSION_INT = 2000004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml index 58ede793b2..6a10654af7 100644 --- a/playbacktests/src/main/AndroidManifest.xml +++ b/playbacktests/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ + android:versionCode="2004" + android:versionName="2.0.4"> From b2222f8cb7bc44d2f69c6037a015923623b7d46a Mon Sep 17 00:00:00 2001 From: vitekn Date: Fri, 21 Oct 2016 13:07:08 +0300 Subject: [PATCH 037/206] Null pointer exception fixed pesPayloadReader can be null here because DefaultStreamReader.init() can return null on unknown streamId. If we have a junk transport stream in our content an exception will be thrown. --- .../google/android/exoplayer2/extractor/ts/TsExtractor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index bac362d711..14b3a0cce6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -467,7 +467,9 @@ public final class TsExtractor implements Extractor { pesPayloadReader = id3Reader; } else { pesPayloadReader = streamReaderFactory.createStreamReader(streamType, esInfo); - pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + if (pesPayloadReader != null) { + pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + } } if (pesPayloadReader != null) { From 0cff74dc704fa1d7fa6fd7d969398e7831384c24 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Oct 2016 05:04:12 -0700 Subject: [PATCH 038/206] Clean up okhttp extension Javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136702598 --- .../ext/okhttp/OkHttpDataSource.java | 12 +++++---- .../ext/okhttp/OkHttpDataSourceFactory.java | 25 ++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index e56072f368..8577d33781 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -65,7 +65,8 @@ public class OkHttpDataSource implements HttpDataSource { private long bytesRead; /** - * @param callFactory A {@link Call.Factory} for use by the source. + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. @@ -76,7 +77,8 @@ public class OkHttpDataSource implements HttpDataSource { } /** - * @param callFactory A {@link Call.Factory} for use by the source. + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from @@ -89,14 +91,14 @@ public class OkHttpDataSource implements HttpDataSource { } /** - * @param callFactory An {@link Call.Factory} for use by the source. + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. * @param listener An optional listener. - * @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control - * header. For example, you could force the network response for all requests. + * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ public OkHttpDataSource(Call.Factory callFactory, String userAgent, Predicate contentTypePredicate, TransferListener listener, diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index a4dd10a8d3..33f204a6f3 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -28,25 +28,38 @@ public final class OkHttpDataSourceFactory implements Factory { private final Call.Factory callFactory; private final String userAgent; - private final TransferListener transferListener; + private final TransferListener listener; private final CacheControl cacheControl; + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the sources created by the factory. + * @param userAgent The User-Agent string that should be used. + * @param listener An optional listener. + */ public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, - TransferListener transferListener) { - this(callFactory, userAgent, transferListener, null); + TransferListener listener) { + this(callFactory, userAgent, listener, null); } + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the sources created by the factory. + * @param userAgent The User-Agent string that should be used. + * @param listener An optional listener. + * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. + */ public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, - TransferListener transferListener, CacheControl cacheControl) { + TransferListener listener, CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; - this.transferListener = transferListener; + this.listener = listener; this.cacheControl = cacheControl; } @Override public OkHttpDataSource createDataSource() { - return new OkHttpDataSource(callFactory, userAgent, null, transferListener, cacheControl); + return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl); } } From 862552c2edf0f1acdebb55a67ae9bd79fe455a08 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 20 Oct 2016 06:29:45 -0700 Subject: [PATCH 039/206] Convert ElementaryStreamReaderFactory into TsPayloadReaderFactory In this CL: * PesReader moves out of TsExtractor and becomes public. * ElementaryStreamReaderFactory becomes TsPayloadReaderFactory and is moved to TsPayloadReader. * The TsPayloadReaderFactory is in charge of wrapping any ElementaryStreamReaders with a PesReader. Incoming: * Extract SectionReader supperclass (analog to PesReader, but for sections) from Pat and Pmt readers. * Add a ScteReader, wrapped by a section reader, and include it in the DefaultTsPayloadReaderFactory. Issue:#726 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136707706 --- .../extractor/ts/AdtsReaderTest.java | 6 +- .../extractor/ts/TsExtractorTest.java | 19 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 4 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 5 +- .../extractor/ts/AdtsExtractor.java | 4 +- .../exoplayer2/extractor/ts/AdtsReader.java | 5 +- ...ava => DefaultTsPayloadReaderFactory.java} | 30 +- .../exoplayer2/extractor/ts/DtsReader.java | 5 +- .../extractor/ts/ElementaryStreamReader.java | 75 +---- .../exoplayer2/extractor/ts/H262Reader.java | 5 +- .../exoplayer2/extractor/ts/H264Reader.java | 5 +- .../exoplayer2/extractor/ts/H265Reader.java | 5 +- .../exoplayer2/extractor/ts/Id3Reader.java | 5 +- .../extractor/ts/MpegAudioReader.java | 5 +- .../exoplayer2/extractor/ts/PesReader.java | 237 +++++++++++++++ .../exoplayer2/extractor/ts/PsExtractor.java | 7 +- .../exoplayer2/extractor/ts/TsExtractor.java | 279 ++---------------- .../extractor/ts/TsPayloadReader.java | 117 ++++++++ .../exoplayer2/source/hls/HlsChunkSource.java | 10 +- 19 files changed, 455 insertions(+), 373 deletions(-) rename library/src/main/java/com/google/android/exoplayer2/extractor/ts/{DefaultStreamReaderFactory.java => DefaultTsPayloadReaderFactory.java} (68%) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index e19de76466..ebb547810b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; @@ -52,7 +52,7 @@ public class AdtsReaderTest extends TestCase { public static final byte[] ADTS_CONTENT = TestUtil.createByteArray( 0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e); - private static final byte TEST_DATA[] = TestUtil.joinByteArrays( + private static final byte[] TEST_DATA = TestUtil.joinByteArrays( ID3_DATA_1, ID3_DATA_2, ADTS_HEADER, @@ -73,7 +73,7 @@ public class AdtsReaderTest extends TestCase { id3Output = fakeExtractorOutput.track(1); adtsReader = new AdtsReader(true); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); - adtsReader.init(fakeExtractorOutput, idGenerator); + adtsReader.createTracks(fakeExtractorOutput, idGenerator); data = new ParsableByteArray(TEST_DATA); firstFeed = true; } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 1f08507599..a455a3b841 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -22,7 +22,8 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -106,7 +107,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { } } - private static final class CustomEsReader extends ElementaryStreamReader { + private static final class CustomEsReader implements ElementaryStreamReader { private final String language; private TrackOutput output; @@ -121,7 +122,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, language, null, 0)); @@ -146,22 +147,22 @@ public final class TsExtractorTest extends InstrumentationTestCase { } - private static final class CustomEsReaderFactory implements ElementaryStreamReader.Factory { + private static final class CustomEsReaderFactory implements TsPayloadReader.Factory { - private final ElementaryStreamReader.Factory defaultFactory; + private final TsPayloadReader.Factory defaultFactory; private CustomEsReader reader; public CustomEsReaderFactory() { - defaultFactory = new DefaultStreamReaderFactory(); + defaultFactory = new DefaultTsPayloadReaderFactory(); } @Override - public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) { + public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { if (streamType == 3) { reader = new CustomEsReader(esInfo.language); - return reader; + return new PesReader(reader); } else { - return defaultFactory.createStreamReader(streamType, esInfo); + return defaultFactory.createPayloadReader(streamType, esInfo); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 7fc8b429a8..dad8214efa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -119,7 +119,7 @@ public final class Ac3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { reader = new Ac3Reader(); // TODO: Add support for embedded ID3. - reader.init(output, new TrackIdGenerator(0, 1)); + reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index a9d3319f87..52faa8c673 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -20,13 +20,14 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. */ -/* package */ final class Ac3Reader extends ElementaryStreamReader { +/* package */ final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -82,7 +83,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { output = extractorOutput.track(generator.getNextId()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 7a9cbd4bb1..76bc4ce66e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -128,7 +128,7 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { reader = new AdtsReader(true); - reader.init(output, new TrackIdGenerator(0, 1)); + reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index d0474f7e44..47cb217fc7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -32,7 +33,7 @@ import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ -/* package */ final class AdtsReader extends ElementaryStreamReader { +/* package */ final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; @@ -106,7 +107,7 @@ import java.util.Collections; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); if (exposeId3) { id3Output = extractorOutput.track(idGenerator.getNextId()); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java similarity index 68% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java rename to library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 58a0e55f02..5aabc29a5d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -16,14 +16,14 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Default implementation for {@link ElementaryStreamReader.Factory}. + * Default implementation for {@link TsPayloadReader.Factory}. */ -public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory { +public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** * Flags controlling elementary stream readers behaviour. @@ -41,39 +41,39 @@ public final class DefaultStreamReaderFactory implements ElementaryStreamReader. @Flags private final int flags; - public DefaultStreamReaderFactory() { + public DefaultTsPayloadReaderFactory() { this(0); } - public DefaultStreamReaderFactory(@Flags int flags) { + public DefaultTsPayloadReaderFactory(@Flags int flags) { this.flags = flags; } @Override - public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) { + public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA_LSF: - return new MpegAudioReader(esInfo.language); + return new PesReader(new MpegAudioReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AAC: return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null - : new AdtsReader(false, esInfo.language); + : new PesReader(new AdtsReader(false, esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: - return new Ac3Reader(esInfo.language); + return new PesReader(new Ac3Reader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: - return new DtsReader(esInfo.language); + return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: - return new H262Reader(); + return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null - : new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0, - (flags & FLAG_DETECT_ACCESS_UNITS) != 0); + : new PesReader(new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0, + (flags & FLAG_DETECT_ACCESS_UNITS) != 0)); case TsExtractor.TS_STREAM_TYPE_H265: - return new H265Reader(); + return new PesReader(new H265Reader()); case TsExtractor.TS_STREAM_TYPE_ID3: - return new Id3Reader(); + return new PesReader(new Id3Reader()); default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 42223ef285..9707685295 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -20,12 +20,13 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous DTS byte stream and extracts individual samples. */ -/* package */ final class DtsReader extends ElementaryStreamReader { +/* package */ final class DtsReader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -77,7 +78,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java index e2efbebb43..57bcf31fc5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -22,82 +22,21 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Extracts individual samples from an elementary media stream, preserving original order. */ -public abstract class ElementaryStreamReader { - - /** - * Factory of {@link ElementaryStreamReader} instances. - */ - public interface Factory { - - /** - * Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the - * stream type is not supported or if the stream already has a reader assigned to it. - * - * @param streamType Stream type value as defined in the PMT entry or associated descriptors. - * @param esInfo Information associated to the elementary stream provided in the PMT. - * @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided - * pid. {@code null} if the stream is not supported or if it should be ignored. - */ - ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo); - - } - - /** - * Holds descriptor information associated with an elementary stream. - */ - public static final class EsInfo { - - public final int streamType; - public String language; - public byte[] descriptorBytes; - - /** - * @param streamType The type of the stream as defined by the - * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. - * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. - * @param descriptorBytes The descriptor bytes associated to the stream. - */ - public EsInfo(int streamType, String language, byte[] descriptorBytes) { - this.streamType = streamType; - this.language = language; - this.descriptorBytes = descriptorBytes; - } - - } - - /** - * Generates track ids for initializing {@link ElementaryStreamReader}s' {@link TrackOutput}s. - */ - public static final class TrackIdGenerator { - - private final int firstId; - private final int idIncrement; - private int generatedIdCount; - - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; - } - - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; - } - - } +public interface ElementaryStreamReader { /** * Notifies the reader that a seek has occurred. */ - public abstract void seek(); + void seek(); /** * Initializes the reader by providing outputs and ids for the tracks. * * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. - * @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the * {@link TrackOutput}s. */ - public abstract void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator); + void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator); /** * Called when a packet starts. @@ -105,18 +44,18 @@ public abstract class ElementaryStreamReader { * @param pesTimeUs The timestamp associated with the packet. * @param dataAlignmentIndicator The data alignment indicator associated with the packet. */ - public abstract void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); + void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); /** * Consumes (possibly partial) data from the current packet. * * @param data The data to consume. */ - public abstract void consume(ParsableByteArray data); + void consume(ParsableByteArray data); /** * Called when a packet ends. */ - public abstract void packetFinished(); + void packetFinished(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index fbfe7e1209..02ea6d7c4e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -29,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H262 byte stream and extracts individual frames. */ -/* package */ final class H262Reader extends ElementaryStreamReader { +/* package */ final class H262Reader implements ElementaryStreamReader { private static final int START_PICTURE = 0x00; private static final int START_SEQUENCE_HEADER = 0xB3; @@ -76,7 +77,7 @@ import java.util.Collections; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 6fee9ea6d7..ed4682d9b9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; @@ -32,7 +33,7 @@ import java.util.List; /** * Parses a continuous H264 byte stream and extracts individual frames. */ -/* package */ final class H264Reader extends ElementaryStreamReader { +/* package */ final class H264Reader implements ElementaryStreamReader { private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set @@ -86,7 +87,7 @@ import java.util.List; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 57d7e77bb7..a78169a054 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -29,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H.265 byte stream and extracts individual frames. */ -/* package */ final class H265Reader extends ElementaryStreamReader { +/* package */ final class H265Reader implements ElementaryStreamReader { private static final String TAG = "H265Reader"; @@ -88,7 +89,7 @@ import java.util.Collections; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); sampleReader = new SampleReader(output); seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 2c657d4aca..c58d847c44 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -19,13 +19,14 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses ID3 data and extracts individual text information frames. */ -/* package */ final class Id3Reader extends ElementaryStreamReader { +/* package */ final class Id3Reader implements ElementaryStreamReader { private static final int ID3_HEADER_SIZE = 10; @@ -51,7 +52,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index d25d0703ae..c67e7ad0ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -20,12 +20,13 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. */ -/* package */ final class MpegAudioReader extends ElementaryStreamReader { +/* package */ final class MpegAudioReader implements ElementaryStreamReader { private static final int STATE_FINDING_HEADER = 0; private static final int STATE_READING_HEADER = 1; @@ -74,7 +75,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; } @Override - public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java new file mode 100644 index 0000000000..d6d2b91292 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2016 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.extractor.ts; + +import android.util.Log; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses PES packet data and extracts samples. + */ +public final class PesReader implements TsPayloadReader { + + private static final String TAG = "PesReader"; + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 10; + private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE) + + private final ElementaryStreamReader reader; + private final ParsableBitArray pesScratch; + + private int state; + private int bytesRead; + + private TimestampAdjuster timestampAdjuster; + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private int payloadSize; + private boolean dataAlignmentIndicator; + private long timeUs; + + public PesReader(ElementaryStreamReader reader) { + this.reader = reader; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + state = STATE_FINDING_HEADER; + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + this.timestampAdjuster = timestampAdjuster; + reader.createTracks(extractorOutput, idGenerator); + } + + // TsPayloadReader implementation. + + @Override + public final void seek() { + state = STATE_FINDING_HEADER; + bytesRead = 0; + seenFirstDts = false; + reader.seek(); + } + + @Override + public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { + if (payloadUnitStartIndicator) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, notify the reader that it has now finished. + reader.packetFinished(); + break; + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skipBytes(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.data, HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.data, readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + reader.packetStarted(timeUs, dataAlignmentIndicator); + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + reader.consume(data); + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + reader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; + } + } + } + + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skipBytes(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); + if (startCodePrefix != 0x000001) { + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); + payloadSize = -1; + return false; + } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1) + dataAlignmentIndicator = pesScratch.readBit(); + pesScratch.skipBits(2); // copyright (1), original_or_copy (1) + ptsFlag = pesScratch.readBit(); + dtsFlag = pesScratch.readBit(); + // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(6); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; + } + + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = C.TIME_UNSET; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' or '0011' + long pts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + if (!seenFirstDts && dtsFlag) { + pesScratch.skipBits(4); // '0011' + long dts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + // Subsequent PES packets may have earlier presentation timestamps than this one, but they + // should all be greater than or equal to this packet's decode timestamp. We feed the + // decode timestamp to the adjuster here so that in the case that this is the first to be + // fed, the adjuster will be able to compute an offset to apply such that the adjusted + // presentation timestamps of all future packets are non-negative. + timestampAdjuster.adjustTsTimestamp(dts); + seenFirstDts = true; + } + timeUs = timestampAdjuster.adjustTsTimestamp(pts); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index b615a3e8ee..6e80f4c49f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TimestampAdjuster; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -202,7 +202,7 @@ public final class PsExtractor implements Extractor { } if (elementaryStreamReader != null) { TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); - elementaryStreamReader.init(output, idGenerator); + elementaryStreamReader.createTracks(output, idGenerator); payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster); psPayloadReaders.put(streamId, payloadReader); } @@ -253,8 +253,7 @@ public final class PsExtractor implements Extractor { private int extendedHeaderLength; private long timeUs; - public PesReader(ElementaryStreamReader pesPayloadReader, - TimestampAdjuster timestampAdjuster) { + public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) { this.pesPayloadReader = pesPayloadReader; this.timestampAdjuster = timestampAdjuster; pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 14b3a0cce6..09806eb343 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -28,8 +27,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; -import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -85,14 +82,14 @@ public final class TsExtractor implements Extractor { private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; - private final ElementaryStreamReader.Factory streamReaderFactory; + private final TsPayloadReader.Factory payloadReaderFactory; private final SparseArray tsPayloadReaders; // Indexed by pid private final SparseBooleanArray trackIds; // Accessed only by the loading thread. private ExtractorOutput output; private boolean tracksEnded; - private ElementaryStreamReader id3Reader; + private TsPayloadReader id3Reader; public TsExtractor() { this(new TimestampAdjuster(0)); @@ -102,19 +99,19 @@ public final class TsExtractor implements Extractor { * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. */ public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultStreamReaderFactory(), false); + this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); } /** * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. - * @param customReaderFactory Factory for injecting a custom set of elementary stream readers. + * @param payloadReaderFactory Factory for injecting a custom set of payload readers. * @param mapByType True if {@link TrackOutput}s should be mapped by their type, false to map them * by their PID. */ public TsExtractor(TimestampAdjuster timestampAdjuster, - ElementaryStreamReader.Factory customReaderFactory, boolean mapByType) { + TsPayloadReader.Factory payloadReaderFactory, boolean mapByType) { this.timestampAdjuster = timestampAdjuster; - this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory); + this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.mapByType = mapByType; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsScratch = new ParsableBitArray(new byte[3]); @@ -258,36 +255,10 @@ public final class TsExtractor implements Extractor { id3Reader = null; } - /** - * Parses TS packet payload data. - */ - private abstract static class TsPayloadReader { - - /** - * Notifies the reader that a seek has occurred. - *

- * Following a call to this method, the data passed to the next invocation of - * {@link #consume(ParsableByteArray, boolean, ExtractorOutput)} will not be a continuation of - * the data that was previously passed. Hence the reader should reset any internal state. - */ - public abstract void seek(); - - /** - * Consumes the payload of a TS packet. - * - * @param data The TS packet. The position will be set to the start of the payload. - * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. - * @param output The output to which parsed data should be written. - */ - public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, - ExtractorOutput output); - - } - /** * Parses Program Association Table data. */ - private class PatReader extends TsPayloadReader { + private class PatReader implements TsPayloadReader { private final ParsableByteArray sectionData; private final ParsableBitArray patScratch; @@ -301,6 +272,12 @@ public final class TsExtractor implements Extractor { patScratch = new ParsableBitArray(new byte[4]); } + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @Override public void seek() { // Do nothing. @@ -361,7 +338,7 @@ public final class TsExtractor implements Extractor { /** * Parses Program Map Table. */ - private class PmtReader extends TsPayloadReader { + private class PmtReader implements TsPayloadReader { private static final int TS_PMT_DESC_REGISTRATION = 0x05; private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; @@ -383,6 +360,12 @@ public final class TsExtractor implements Extractor { this.pid = pid; } + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @Override public void seek() { // Do nothing. @@ -437,8 +420,9 @@ public final class TsExtractor implements Extractor { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); - id3Reader = streamReaderFactory.createStreamReader(TS_STREAM_TYPE_ID3, dummyEsInfo); - id3Reader.init(output, new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); + id3Reader.init(timestampAdjuster, output, + new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */ @@ -462,18 +446,18 @@ public final class TsExtractor implements Extractor { } trackIds.put(trackId, true); - ElementaryStreamReader pesPayloadReader; + TsPayloadReader reader; if (mapByType && streamType == TS_STREAM_TYPE_ID3) { - pesPayloadReader = id3Reader; + reader = id3Reader; } else { - pesPayloadReader = streamReaderFactory.createStreamReader(streamType, esInfo); - if (pesPayloadReader != null) { - pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); + if (reader != null) { + reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); } } - if (pesPayloadReader != null) { - tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster)); + if (reader != null) { + tsPayloadReaders.put(elementaryPid, reader); } } if (mapByType) { @@ -534,208 +518,5 @@ public final class TsExtractor implements Extractor { } - /** - * Parses PES packet data and extracts samples. - */ - private static final class PesReader extends TsPayloadReader { - - private static final int STATE_FINDING_HEADER = 0; - private static final int STATE_READING_HEADER = 1; - private static final int STATE_READING_HEADER_EXTENSION = 2; - private static final int STATE_READING_BODY = 3; - - private static final int HEADER_SIZE = 9; - private static final int MAX_HEADER_EXTENSION_SIZE = 10; - private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE) - - private final ElementaryStreamReader pesPayloadReader; - private final TimestampAdjuster timestampAdjuster; - private final ParsableBitArray pesScratch; - - private int state; - private int bytesRead; - - private boolean ptsFlag; - private boolean dtsFlag; - private boolean seenFirstDts; - private int extendedHeaderLength; - private int payloadSize; - private boolean dataAlignmentIndicator; - private long timeUs; - - public PesReader(ElementaryStreamReader pesPayloadReader, - TimestampAdjuster timestampAdjuster) { - this.pesPayloadReader = pesPayloadReader; - this.timestampAdjuster = timestampAdjuster; - pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); - state = STATE_FINDING_HEADER; - } - - @Override - public void seek() { - state = STATE_FINDING_HEADER; - bytesRead = 0; - seenFirstDts = false; - pesPayloadReader.seek(); - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, - ExtractorOutput output) { - if (payloadUnitStartIndicator) { - switch (state) { - case STATE_FINDING_HEADER: - case STATE_READING_HEADER: - // Expected. - break; - case STATE_READING_HEADER_EXTENSION: - Log.w(TAG, "Unexpected start indicator reading extended header"); - break; - case STATE_READING_BODY: - // If payloadSize == -1 then the length of the previous packet was unspecified, and so - // we only know that it's finished now that we've seen the start of the next one. This - // is expected. If payloadSize != -1, then the length of the previous packet was known, - // but we didn't receive that amount of data. This is not expected. - if (payloadSize != -1) { - Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); - } - // Either way, notify the reader that it has now finished. - pesPayloadReader.packetFinished(); - break; - } - setState(STATE_READING_HEADER); - } - - while (data.bytesLeft() > 0) { - switch (state) { - case STATE_FINDING_HEADER: - data.skipBytes(data.bytesLeft()); - break; - case STATE_READING_HEADER: - if (continueRead(data, pesScratch.data, HEADER_SIZE)) { - setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); - } - break; - case STATE_READING_HEADER_EXTENSION: - int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); - // Read as much of the extended header as we're interested in, and skip the rest. - if (continueRead(data, pesScratch.data, readLength) - && continueRead(data, null, extendedHeaderLength)) { - parseHeaderExtension(); - pesPayloadReader.packetStarted(timeUs, dataAlignmentIndicator); - setState(STATE_READING_BODY); - } - break; - case STATE_READING_BODY: - readLength = data.bytesLeft(); - int padding = payloadSize == -1 ? 0 : readLength - payloadSize; - if (padding > 0) { - readLength -= padding; - data.setLimit(data.getPosition() + readLength); - } - pesPayloadReader.consume(data); - if (payloadSize != -1) { - payloadSize -= readLength; - if (payloadSize == 0) { - pesPayloadReader.packetFinished(); - setState(STATE_READING_HEADER); - } - } - break; - } - } - } - - private void setState(int state) { - this.state = state; - bytesRead = 0; - } - - /** - * Continues a read from the provided {@code source} into a given {@code target}. It's assumed - * that the data should be written into {@code target} starting from an offset of zero. - * - * @param source The source from which to read. - * @param target The target into which data is to be read, or {@code null} to skip. - * @param targetLength The target length of the read. - * @return Whether the target length has been reached. - */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { - int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); - if (bytesToRead <= 0) { - return true; - } else if (target == null) { - source.skipBytes(bytesToRead); - } else { - source.readBytes(target, bytesRead, bytesToRead); - } - bytesRead += bytesToRead; - return bytesRead == targetLength; - } - - private boolean parseHeader() { - // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of - // the header. - pesScratch.setPosition(0); - int startCodePrefix = pesScratch.readBits(24); - if (startCodePrefix != 0x000001) { - Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - payloadSize = -1; - return false; - } - - pesScratch.skipBits(8); // stream_id. - int packetLength = pesScratch.readBits(16); - pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1) - dataAlignmentIndicator = pesScratch.readBit(); - pesScratch.skipBits(2); // copyright (1), original_or_copy (1) - ptsFlag = pesScratch.readBit(); - dtsFlag = pesScratch.readBit(); - // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), - // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) - pesScratch.skipBits(6); - extendedHeaderLength = pesScratch.readBits(8); - - if (packetLength == 0) { - payloadSize = -1; - } else { - payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ - - HEADER_SIZE - extendedHeaderLength; - } - return true; - } - - private void parseHeaderExtension() { - pesScratch.setPosition(0); - timeUs = C.TIME_UNSET; - if (ptsFlag) { - pesScratch.skipBits(4); // '0010' or '0011' - long pts = (long) pesScratch.readBits(3) << 30; - pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBits(15) << 15; - pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBits(15); - pesScratch.skipBits(1); // marker_bit - if (!seenFirstDts && dtsFlag) { - pesScratch.skipBits(4); // '0011' - long dts = (long) pesScratch.readBits(3) << 30; - pesScratch.skipBits(1); // marker_bit - dts |= pesScratch.readBits(15) << 15; - pesScratch.skipBits(1); // marker_bit - dts |= pesScratch.readBits(15); - pesScratch.skipBits(1); // marker_bit - // Subsequent PES packets may have earlier presentation timestamps than this one, but they - // should all be greater than or equal to this packet's decode timestamp. We feed the - // decode timestamp to the adjuster here so that in the case that this is the first to be - // fed, the adjuster will be able to compute an offset to apply such that the adjusted - // presentation timestamps of all future packets are non-negative. - timestampAdjuster.adjustTsTimestamp(dts); - seenFirstDts = true; - } - timeUs = timestampAdjuster.adjustTsTimestamp(pts); - } - } - - } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java new file mode 100644 index 0000000000..ac0a37fb7a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 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.extractor.ts; + +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses TS packet payload data. + */ +public interface TsPayloadReader { + + /** + * Factory of {@link TsPayloadReader} instances. + */ + interface Factory { + + /** + * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information. + * May return null if the stream type is not supported. + * + * @param streamType Stream type value as defined in the PMT entry or associated descriptors. + * @param esInfo Information associated to the elementary stream provided in the PMT. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * {@code null} if the stream is not supported. + */ + TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); + + } + + /** + * Holds information associated with a PMT entry. + */ + final class EsInfo { + + public final int streamType; + public final String language; + public final byte[] descriptorBytes; + + /** + * @param streamType The type of the stream as defined by the + * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. + * @param descriptorBytes The descriptor bytes associated to the stream. + */ + public EsInfo(int streamType, String language, byte[] descriptorBytes) { + this.streamType = streamType; + this.language = language; + this.descriptorBytes = descriptorBytes; + } + + } + + /** + * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s. + */ + final class TrackIdGenerator { + + private final int firstId; + private final int idIncrement; + private int generatedIdCount; + + public TrackIdGenerator(int firstId, int idIncrement) { + this.firstId = firstId; + this.idIncrement = idIncrement; + } + + public int getNextId() { + return firstId + idIncrement * generatedIdCount++; + } + + } + + /** + * Initializes the payload reader. + * + * @param timestampAdjuster + * @param extractorOutput + * @param idGenerator + */ + void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator); + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, boolean, ExtractorOutput)} will not be a continuation of + * the data that was previously passed. Hence the reader should reset any internal state. + */ + void seek(); + + /** + * Consumes the payload of a TS packet. + * + * @param data The TS packet. The position will be set to the start of the payload. + * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @param output The output to which parsed data should be written. + */ + void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index a23ab3bae7..7ef16f361d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer2.extractor.ts.DefaultStreamReaderFactory; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; @@ -384,7 +384,7 @@ import java.util.Locale; timestampAdjuster = timestampAdjusterProvider.getAdjuster( segment.discontinuitySequenceNumber, startTimeUs); // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultStreamReaderFactory.Flags + @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = 0; String codecs = variants[newVariantIndex].format.codecs; if (!TextUtils.isEmpty(codecs)) { @@ -392,14 +392,14 @@ import java.util.Locale; // exist. If we know from the codec attribute that they don't exist, then we can // explicitly ignore them even if they're declared. if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_AAC_STREAM; + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; } if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_H264_STREAM; + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; } } extractor = new TsExtractor(timestampAdjuster, - new DefaultStreamReaderFactory(esReaderFactoryFlags), true); + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); } } else { // MPEG-2 TS segments, and we need to continue using the same extractor. From a0fe258e8d70a93ed8fd9cd296be44625ea03669 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 20 Oct 2016 07:08:38 -0700 Subject: [PATCH 040/206] Fix/suppress some analysis warnings. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136710546 --- extensions/okhttp/build.gradle | 2 +- .../exoplayer2/ext/vp9/VpxRenderer.java | 5 ++- library/build.gradle | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 9 +++--- .../android/exoplayer2/audio/AudioTrack.java | 7 +++++ .../audio/MediaCodecAudioRenderer.java | 3 +- .../exoplayer2/extractor/mkv/Sniffer.java | 2 +- .../extractor/ogg/VorbisBitArray.java | 2 +- .../extractor/rawcc/RawCcExtractor.java | 8 ++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 13 ++++---- .../exoplayer2/metadata/id3/Id3Decoder.java | 3 +- .../exoplayer2/source/MergingMediaPeriod.java | 6 ++-- .../exoplayer2/source/hls/HlsMediaPeriod.java | 3 +- .../text/SimpleSubtitleOutputBuffer.java | 2 +- .../exoplayer2/text/cea/Cea608Decoder.java | 4 ++- .../exoplayer2/text/ttml/TtmlRenderUtil.java | 31 ++++++++++--------- .../text/webvtt/WebvttCueParser.java | 31 ++++++++++--------- .../trackselection/MappingTrackSelector.java | 4 +-- .../upstream/RawResourceDataSource.java | 2 +- .../res/layout/exo_playback_control_view.xml | 2 ++ .../playbacktests/gts/DashTest.java | 6 ++-- .../playbacktests/util/ExoHostedTest.java | 8 +++-- .../playbacktests/util/HostActivity.java | 2 +- 23 files changed, 87 insertions(+), 70 deletions(-) diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index c7555e9ced..442f0f78dc 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -37,7 +37,7 @@ android { dependencies { compile project(':library') - compile('com.squareup.okhttp3:okhttp:+') { + compile('com.squareup.okhttp3:okhttp:3.4.1') { exclude group: 'org.json' } } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java index a0eccb41a7..d108ae8b4f 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java @@ -73,10 +73,13 @@ import javax.microedition.khronos.opengles.GL10; private final int[] yuvTextures = new int[3]; private final AtomicReference pendingOutputBufferReference; + // Kept in a field rather than a local variable so that it doesn't get garbage collected before + // glDrawArrays uses it. + @SuppressWarnings("FieldCanBeLocal") + private FloatBuffer textureCoords; private int program; private int texLocation; private int colorMatrixLocation; - private FloatBuffer textureCoords; private int previousWidth; private int previousStride; diff --git a/library/build.gradle b/library/build.gradle index 42e7cb3bb1..aaadc9cdd4 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -55,7 +55,7 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' - compile 'com.android.support:support-annotations:24.2.0' + compile 'com.android.support:support-annotations:24.2.1' } android.libraryVariants.all { variant -> diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 56b862073a..9d6c435635 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -249,6 +249,7 @@ import java.io.IOException; // Handler.Callback implementation. + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { try { @@ -335,8 +336,7 @@ import java.io.IOException; } } - private void prepareInternal(MediaSource mediaSource, boolean resetPosition) - throws ExoPlaybackException { + private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { resetInternal(); loadControl.onPrepared(); if (resetPosition) { @@ -884,8 +884,7 @@ import java.io.IOException; } } - private void attemptRestart(Timeline newTimeline, Timeline oldTimeline, - int oldPeriodIndex) throws ExoPlaybackException { + private void attemptRestart(Timeline newTimeline, Timeline oldTimeline, int oldPeriodIndex) { int newPeriodIndex = C.INDEX_UNSET; while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { @@ -1260,7 +1259,7 @@ import java.io.IOException; } public long updatePeriodTrackSelection(long positionUs, LoadControl loadControl, - boolean forceRecreateStreams, boolean[] streamResetFlags) throws ExoPlaybackException { + boolean forceRecreateStreams, boolean[] streamResetFlags) { for (int i = 0; i < trackSelections.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 87f6546e1f..1eff48e28d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -23,6 +23,7 @@ import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -981,6 +982,9 @@ public final class AudioTrack { case C.ENCODING_PCM_32BIT: resampledSize = size / 2; break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: // Never happens. throw new IllegalStateException(); @@ -1016,6 +1020,9 @@ public final class AudioTrack { resampledBuffer.put(buffer.get(i + 3)); } break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: // Never happens. throw new IllegalStateException(); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 66dd010a6f..5862e7e218 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -76,7 +76,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * has obtained the keys necessary to decrypt encrypted regions of the media. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java index 3d3e677881..a3fde6d455 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -40,7 +40,7 @@ import java.io.IOException; } /** - * @see Extractor#sniff + * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) */ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { long inputLength = input.getLength(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java index c6c9efc0f7..ae52e80299 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; /* package */ final class VorbisBitArray { public final byte[] data; - private int limit; + private final int limit; private int byteOffset; private int bitOffset; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index ea9458a657..452d09e132 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -47,7 +47,6 @@ public final class RawCcExtractor implements Extractor { private final ParsableByteArray dataScratch; - private ExtractorOutput extractorOutput; private TrackOutput trackOutput; private int parserState; @@ -63,10 +62,9 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - this.extractorOutput = output; - extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = extractorOutput.track(0); - extractorOutput.endTracks(); + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + trackOutput = output.track(0); + output.endTracks(); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 2f9524b7f0..1bfcb4418e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -23,6 +23,7 @@ import android.media.MediaCodecList; import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import android.util.SparseIntArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -63,8 +64,8 @@ public final class MediaCodecUtil { // Codecs to constant mappings. // AVC. - private static final Map AVC_PROFILE_NUMBER_TO_CONST; - private static final Map AVC_LEVEL_NUMBER_TO_CONST; + private static final SparseIntArray AVC_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST; private static final String CODEC_ID_AVC1 = "avc1"; private static final String CODEC_ID_AVC2 = "avc2"; // HEVC. @@ -364,8 +365,8 @@ public final class MediaCodecUtil { Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); return null; } - Integer profileInteger = null; - Integer levelInteger = null; + Integer profileInteger; + Integer levelInteger; try { if (codecsParts[1].length() == 6) { // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal. @@ -555,13 +556,13 @@ public final class MediaCodecUtil { } static { - AVC_PROFILE_NUMBER_TO_CONST = new HashMap<>(); + AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain); AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended); AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh); - AVC_LEVEL_NUMBER_TO_CONST = new HashMap<>(); + AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1); // TODO: Find int for CodecProfileLevel.AVCLevel1b. AVC_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AVCLevel11); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 92c6efb530..35960e39d2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.id3; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.MimeTypes; @@ -124,7 +123,7 @@ public final class Id3Decoder implements MetadataDecoder> { /** * @param id3Buffer A {@link ParsableByteArray} from which data should be read. * @return The size of ID3 frames in bytes, excluding the header and footer. - * @throws ParserException If ID3 file identifier != "ID3". + * @throws MetadataDecoderException If ID3 file identifier != "ID3". */ private static int decodeId3Header(ParsableByteArray id3Buffer) throws MetadataDecoderException { int id1 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index cfab4b14aa..10c56e5576 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -149,9 +149,9 @@ import java.util.IdentityHashMap; } // It must be possible to seek enabled periods to the new position, if there is one. if (positionUs != C.TIME_UNSET) { - for (int i = 0; i < enabledPeriods.length; i++) { - if (enabledPeriods[i] != periods[0] - && enabledPeriods[i].seekToUs(positionUs) != positionUs) { + for (MediaPeriod enabledPeriod : enabledPeriods) { + if (enabledPeriod != periods[0] + && enabledPeriod.seekToUs(positionUs) != positionUs) { throw new IllegalStateException("Children seeked to different positions"); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index f4c8177f21..598fa9b281 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -69,7 +69,6 @@ import java.util.List; private int pendingPrepareCount; private HlsPlaylist playlist; private boolean seenFirstTrackSelection; - private long durationUs; private boolean isLive; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; @@ -280,7 +279,7 @@ import java.util.List; } // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. - durationUs = sampleStreamWrappers[0].getDurationUs(); + long durationUs = sampleStreamWrappers[0].getDurationUs(); isLive = sampleStreamWrappers[0].isLive(); int totalTrackGroupCount = 0; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java index 1a69cd7ebd..b2c25631f4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java @@ -20,7 +20,7 @@ package com.google.android.exoplayer2.text; */ /* package */ final class SimpleSubtitleOutputBuffer extends SubtitleOutputBuffer { - private SimpleSubtitleDecoder owner; + private final SimpleSubtitleDecoder owner; /** * @param owner The decoder that owns this buffer. diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 2715b0cbe0..5ff68c2781 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public final class Cea608Decoder extends CeaDecoder { private static final int NTSC_CC_FIELD_1 = 0x00; + private static final int CC_TYPE_MASK = 0x03; private static final int CC_VALID_FLAG = 0x04; private static final int PAYLOAD_TYPE_CC = 4; @@ -223,7 +224,8 @@ public final class Cea608Decoder extends CeaDecoder { byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // Only examine valid NTSC_CC_FIELD_1 packets - if (ccTypeAndValid != (CC_VALID_FLAG | NTSC_CC_FIELD_1)) { + if ((ccTypeAndValid & CC_VALID_FLAG) == 0 + || (ccTypeAndValid & CC_TYPE_MASK) != NTSC_CC_FIELD_1) { // TODO: Add support for NTSC_CC_FIELD_2 packets continue; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index bb89b05603..21333081c6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -92,21 +92,22 @@ import java.util.Map; builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getFontSizeUnit() != TtmlStyle.UNSPECIFIED) { - switch (style.getFontSizeUnit()) { - case TtmlStyle.FONT_SIZE_UNIT_PIXEL: - builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - case TtmlStyle.FONT_SIZE_UNIT_EM: - builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - case TtmlStyle.FONT_SIZE_UNIT_PERCENT: - builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - } + switch (style.getFontSizeUnit()) { + case TtmlStyle.FONT_SIZE_UNIT_PIXEL: + builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.FONT_SIZE_UNIT_EM: + builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.FONT_SIZE_UNIT_PERCENT: + builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.UNSPECIFIED: + // Do nothing. + break; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index b7345e0b5f..c63004e1cd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -413,21 +413,22 @@ import java.util.regex.Pattern; spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getFontSizeUnit() != WebvttCssStyle.UNSPECIFIED) { - switch (style.getFontSizeUnit()) { - case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: - spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - case WebvttCssStyle.FONT_SIZE_UNIT_EM: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - break; - } + switch (style.getFontSizeUnit()) { + case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: + spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case WebvttCssStyle.FONT_SIZE_UNIT_EM: + spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: + spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case WebvttCssStyle.UNSPECIFIED: + // Do nothing. + break; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 3826ee4668..3307fc3baa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -70,8 +70,8 @@ public abstract class MappingTrackSelector extends TrackSelector buildDrmSessionManager( + final String userAgent) { + StreamingDrmSessionManager drmSessionManager = null; if (isWidevineEncrypted) { try { // Force L3 if secure decoder is not available. diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index b8ac1eb76c..6d834873ea 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; @@ -130,7 +131,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); trackSelector = buildTrackSelector(host, bandwidthMeter); String userAgent = "ExoPlayerPlaybackTests"; - DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); + DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); player = buildExoPlayer(host, surface, trackSelector, drmSessionManager); player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), bandwidthMeter)); player.addListener(this); @@ -296,7 +297,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen // Internal logic - protected DrmSessionManager buildDrmSessionManager(String userAgent) { + protected DrmSessionManager buildDrmSessionManager(String userAgent) { // Do nothing. Interested subclasses may override. return null; } @@ -309,7 +310,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @SuppressWarnings("unused") protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, - MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(host, trackSelector, new DefaultLoadControl(), drmSessionManager, false, 0); player.setVideoSurface(surface); diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java index 2a890b7c7f..9c2ced3a8a 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java @@ -232,7 +232,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba } @SuppressLint("InlinedApi") - private static final int getWifiLockMode() { + private static int getWifiLockMode() { return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF; } From eeb37d73e7436a9f9be4fb53d36d843b82338a4e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 21 Oct 2016 03:46:41 -0700 Subject: [PATCH 041/206] Create the SectionPayloadReader interface SectionPayloadReaders are provided to a SectionReader to get whole sections(crc checked). This allows the injection of custom section readers. E.G: SCTE35 messages. Issue:#726 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136816612 --- .../exoplayer2/extractor/ts/PesReader.java | 3 +- .../extractor/ts/SectionPayloadReader.java | 33 +++++ .../extractor/ts/SectionReader.java | 89 +++++++++++++ .../exoplayer2/extractor/ts/TsExtractor.java | 123 +++--------------- .../extractor/ts/TsPayloadReader.java | 7 +- 5 files changed, 142 insertions(+), 113 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index d6d2b91292..598394a870 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -78,8 +78,7 @@ public final class PesReader implements TsPayloadReader { } @Override - public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, - ExtractorOutput output) { + public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator) { switch (state) { case STATE_FINDING_HEADER: diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java new file mode 100644 index 0000000000..9be41af594 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 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.extractor.ts; + +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Reads section data. + */ +public interface SectionPayloadReader { + + /** + * Called by a {@link SectionReader} when a full section is received. + * + * @param sectionData The data belonging to a section, including the section header but excluding + * the CRC_32 field. + */ + void consume(ParsableByteArray sectionData); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java new file mode 100644 index 0000000000..ccf00f8d19 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 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.extractor.ts; + +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +/** + * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}. + */ +public final class SectionReader implements TsPayloadReader { + + private static final int SECTION_HEADER_LENGTH = 3; + + private final ParsableByteArray sectionData; + private final ParsableBitArray headerScratch; + private final SectionPayloadReader reader; + private int sectionLength; + private int sectionBytesRead; + + public SectionReader(SectionPayloadReader reader) { + this.reader = reader; + sectionData = new ParsableByteArray(); + headerScratch = new ParsableBitArray(new byte[SECTION_HEADER_LENGTH]); + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // TODO: Injectable section readers might want to generate metadata tracks. + // Do nothing. + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skipBytes(pointerField); + + // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of + // the header. + data.readBytes(headerScratch, SECTION_HEADER_LENGTH); + data.setPosition(data.getPosition() - SECTION_HEADER_LENGTH); + headerScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), 0 (1), reserved (2) + sectionLength = headerScratch.readBits(12) + SECTION_HEADER_LENGTH; + sectionBytesRead = 0; + + sectionData.reset(sectionLength); + } + + int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); + data.readBytes(sectionData.data, sectionBytesRead, bytesToRead); + sectionBytesRead += bytesToRead; + if (sectionBytesRead < sectionLength) { + // Not yet fully read. + return; + } + + if (Util.crc(sectionData.data, 0, sectionLength, 0xFFFFFFFF) != 0) { + // CRC Invalid. The section gets discarded. + return; + } + sectionData.setLimit(sectionData.limit() - 4); // Exclude the CRC_32 field. + reader.consume(sectionData); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 09806eb343..b52b5cc047 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -27,6 +27,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -236,7 +238,7 @@ public final class TsExtractor implements Extractor { payloadReader.seek(); } tsPacketBuffer.setLimit(endOfPacket); - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output); + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket); tsPacketBuffer.setLimit(limit); } @@ -251,75 +253,29 @@ public final class TsExtractor implements Extractor { private void resetPayloadReaders() { trackIds.clear(); tsPayloadReaders.clear(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader())); id3Reader = null; } /** * Parses Program Association Table data. */ - private class PatReader implements TsPayloadReader { + private class PatReader implements SectionPayloadReader { - private final ParsableByteArray sectionData; private final ParsableBitArray patScratch; - private int sectionLength; - private int sectionBytesRead; - private int crc; - public PatReader() { - sectionData = new ParsableByteArray(); patScratch = new ParsableBitArray(new byte[4]); } @Override - public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, - TrackIdGenerator idGenerator) { - // Do nothing. - } - - @Override - public void seek() { - // Do nothing. - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, - ExtractorOutput output) { - // Skip pointer. - if (payloadUnitStartIndicator) { - int pointerField = data.readUnsignedByte(); - data.skipBytes(pointerField); - - // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of - // the header. - data.readBytes(patScratch, 3); - patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), 0 (1), reserved (2) - sectionLength = patScratch.readBits(12); - sectionBytesRead = 0; - crc = Util.crc(patScratch.data, 0, 3, 0xFFFFFFFF); - - sectionData.reset(sectionLength); - } - - int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); - data.readBytes(sectionData.data, sectionBytesRead, bytesToRead); - sectionBytesRead += bytesToRead; - if (sectionBytesRead < sectionLength) { - // Not yet fully read. - return; - } - - if (Util.crc(sectionData.data, 0, sectionLength, crc) != 0) { - // CRC Invalid. The section gets discarded. - return; - } - + public void consume(ParsableByteArray sectionData) { + // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), // section_number (8), last_section_number (8) - sectionData.skipBytes(5); + sectionData.skipBytes(8); - int programCount = (sectionLength - 9) / 4; + int programCount = sectionData.bytesLeft() / 4; for (int i = 0; i < programCount; i++) { sectionData.readBytes(patScratch, 4); int programNumber = patScratch.readBits(16); @@ -328,7 +284,7 @@ public final class TsExtractor implements Extractor { patScratch.skipBits(13); // network_PID (13) } else { int pid = patScratch.readBits(13); - tsPayloadReaders.put(pid, new PmtReader(pid)); + tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); } } } @@ -338,7 +294,7 @@ public final class TsExtractor implements Extractor { /** * Parses Program Map Table. */ - private class PmtReader implements TsPayloadReader { + private class PmtReader implements SectionPayloadReader { private static final int TS_PMT_DESC_REGISTRATION = 0x05; private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; @@ -347,66 +303,20 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_DTS = 0x7B; private final ParsableBitArray pmtScratch; - private final ParsableByteArray sectionData; private final int pid; - private int sectionLength; - private int sectionBytesRead; - private int crc; - public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); - sectionData = new ParsableByteArray(); this.pid = pid; } @Override - public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, - TrackIdGenerator idGenerator) { - // Do nothing. - } - - @Override - public void seek() { - // Do nothing. - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, - ExtractorOutput output) { - if (payloadUnitStartIndicator) { - // Skip pointer. - int pointerField = data.readUnsignedByte(); - data.skipBytes(pointerField); - - // Note: see ISO/IEC 13818-1, section 2.4.4.8 for detailed information on the format of - // the header. - data.readBytes(pmtScratch, 3); - pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), 0 (1), reserved (2) - sectionLength = pmtScratch.readBits(12); - sectionBytesRead = 0; - crc = Util.crc(pmtScratch.data, 0, 3, 0xFFFFFFFF); - - sectionData.reset(sectionLength); - } - - int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); - data.readBytes(sectionData.data, sectionBytesRead, bytesToRead); - sectionBytesRead += bytesToRead; - if (sectionBytesRead < sectionLength) { - // Not yet fully read. - return; - } - - if (Util.crc(sectionData.data, 0, sectionLength, crc) != 0) { - // CRC Invalid. The section gets discarded. - return; - } - + public void consume(ParsableByteArray sectionData) { + // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), // program_number (16), reserved (2), version_number (5), current_next_indicator (1), // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) // Skip the rest of the PMT header. - sectionData.skipBytes(7); + sectionData.skipBytes(10); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -425,8 +335,7 @@ public final class TsExtractor implements Extractor { new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } - int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */ - - programInfoLength - 4 /* CRC length */; + int remainingEntriesLength = sectionData.bytesLeft(); while (remainingEntriesLength > 0) { sectionData.readBytes(pmtScratch, 5); int streamType = pmtScratch.readBits(8); @@ -513,7 +422,7 @@ public final class TsExtractor implements Extractor { } data.setPosition(descriptorsEndPosition); return new EsInfo(streamType, language, - Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition)); + Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index ac0a37fb7a..28e9fb9095 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -100,8 +100,8 @@ public interface TsPayloadReader { * Notifies the reader that a seek has occurred. *

* Following a call to this method, the data passed to the next invocation of - * {@link #consume(ParsableByteArray, boolean, ExtractorOutput)} will not be a continuation of - * the data that was previously passed. Hence the reader should reset any internal state. + * {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was + * previously passed. Hence the reader should reset any internal state. */ void seek(); @@ -110,8 +110,7 @@ public interface TsPayloadReader { * * @param data The TS packet. The position will be set to the start of the payload. * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. - * @param output The output to which parsed data should be written. */ - void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output); + void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); } From 6e69b985175afcc692ae4b4f1ed89ab3f993712c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Jul 2016 08:49:41 +0100 Subject: [PATCH 042/206] Avoid throwing an exception when an ID3 header is not found Issue:#1966 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136836332 --- .../android/exoplayer2/extractor/ts/Id3Reader.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index c58d847c44..c19bc9d14e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -28,6 +29,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ /* package */ final class Id3Reader implements ElementaryStreamReader { + private static final String TAG = "Id3Reader"; + private static final int ID3_HEADER_SIZE = 10; private final ParsableByteArray id3Header; @@ -82,7 +85,14 @@ import com.google.android.exoplayer2.util.ParsableByteArray; headerBytesAvailable); if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { // We've finished reading the ID3 header. Extract the sample size. - id3Header.setPosition(6); // 'ID3' (3) + version (2) + flags (1) + id3Header.setPosition(0); + if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() + || '3' != id3Header.readUnsignedByte()) { + Log.w(TAG, "Discarding invalid ID3 tag"); + writingSample = false; + return; + } + id3Header.skipBytes(3); // version (2) + flags (1) sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); } } From 00f9fc6728f961dc55fcfff1db880811b5252966 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 24 Oct 2016 18:01:48 +0100 Subject: [PATCH 043/206] Don't propagate GaplessInfoHolder when parsing mp4 metadata --- .../extractor/GaplessInfoHolder.java | 2 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 29 +++++++------------ .../extractor/mp4/Mp4Extractor.java | 5 +++- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 4b5fa977ee..7e2a1b4a23 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -94,7 +94,7 @@ public final class GaplessInfoHolder { * @param data The comment's payload data. * @return Whether the holder was populated. */ - public boolean setFromComment(String name, String data) { + private boolean setFromComment(String name, String data) { if (!GAPLESS_COMMENT_ID.equals(name)) { return false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index d91d677f87..da29305311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -407,10 +407,8 @@ import java.util.List; * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. - * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ - public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, - GaplessInfoHolder out) { + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. @@ -424,14 +422,14 @@ import java.util.List; if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData, out); + return parseMetaAtom(udtaData); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { + private static Metadata parseMetaAtom(ParsableByteArray data) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { @@ -440,7 +438,7 @@ import java.util.List; if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); - Metadata metadata = parseIlst(ilst, out); + Metadata metadata = parseIlst(ilst); if (metadata != null) { return metadata; } @@ -450,13 +448,13 @@ import java.util.List; return null; } - private static Metadata parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { + private static Metadata parseIlst(ParsableByteArray ilst) { ArrayList entries = new ArrayList<>(); while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, entries, out); + parseIlstElement(ilst, type, endPosition, entries); ilst.setPosition(endPosition); } return entries.isEmpty() ? null : new Metadata(entries); @@ -506,7 +504,7 @@ import java.util.List; // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, - List builder, GaplessInfoHolder out) { + List builder) { if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { parseTextAttribute(builder, "TIT2", ilst); } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { @@ -557,7 +555,7 @@ import java.util.List; } else if (type == TYPE_SHOW) { parseTextAttribute(builder, "show", ilst); } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition, out); + parseExtendedAttribute(builder, ilst, endPosition); } } @@ -678,7 +676,7 @@ import java.util.List; } private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, - int endPosition, GaplessInfoHolder out) { + int endPosition) { String domain = null; String name = null; Object value = null; @@ -699,14 +697,9 @@ import java.util.List; } if (value != null) { - if (!out.hasGaplessInfo() && Util.areEqual(domain, "com.apple.iTunes")) { - String s = value instanceof byte[] ? new String((byte[]) value) : value.toString(); - out.setFromComment(name, s); - } - - if (Util.areEqual(domain, "com.apple.iTunes") && Util.areEqual(name, "iTunNORM") && (value instanceof byte[])) { + if (Util.areEqual(domain, "com.apple.iTunes")) { String s = new String((byte[]) value); - Id3Frame frame = new CommentFrame("eng", "iTunNORM", s); + Id3Frame frame = new CommentFrame("eng", name, s); builder.add(frame); } else if (domain != null && name != null) { String extendedName = domain + "." + name; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 6107a9ad75..4c52622c78 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -315,7 +315,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - metadata = AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); + metadata = AtomParsers.parseUdta(udta, isQuickTime); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } } for (int i = 0; i < moov.containerChildren.size(); i++) { From 819ebf703abb61b1ac3f4a60359a45266ac49cb4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Oct 2016 04:04:55 -0700 Subject: [PATCH 044/206] Assign track type TEXT for CEA708 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137011920 --- .../com/google/android/exoplayer2/util/MimeTypes.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 4776e4d008..e289a5bac1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -211,10 +211,10 @@ public final class MimeTypes { } else if (isVideo(mimeType)) { return C.TRACK_TYPE_VIDEO; } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType) - || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType) - || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType) - || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType) - || APPLICATION_PGS.equals(mimeType)) { + || APPLICATION_CEA708.equals(mimeType) || APPLICATION_SUBRIP.equals(mimeType) + || APPLICATION_TTML.equals(mimeType) || APPLICATION_TX3G.equals(mimeType) + || APPLICATION_MP4VTT.equals(mimeType) || APPLICATION_RAWCC.equals(mimeType) + || APPLICATION_VOBSUB.equals(mimeType) || APPLICATION_PGS.equals(mimeType)) { return C.TRACK_TYPE_TEXT; } else if (APPLICATION_ID3.equals(mimeType)) { return C.TRACK_TYPE_METADATA; From 790e5fd2a1110e9440db40447e564ac393cb99b5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Oct 2016 08:24:41 -0700 Subject: [PATCH 045/206] Update gradle wrapper Code coverage is disabled in V2 to workaround https://code.google.com/p/android/issues/detail?id=226070 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137032839 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- library/build.gradle | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 8e9032be70..9d14c36b1b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.2.1' classpath 'com.novoda:bintray-release:0.3.4' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 04363c87a9..c41838fae2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Sep 01 11:39:15 BST 2016 +#Mon Oct 24 14:40:37 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/library/build.gradle b/library/build.gradle index aaadc9cdd4..dd1d5c3c87 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -35,9 +35,11 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://code.google.com/p/android/issues/detail?id=226070 + // debug { + // testCoverageEnabled = true + // } } lintOptions { From 5c83c28a1fa7c0b2c04eb5d38390484adc4c7e5b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Oct 2016 08:48:17 -0700 Subject: [PATCH 046/206] Blacklist another non-OMX decoder Issue: #1986 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137035576 --- .../com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 1bfcb4418e..4836953f8f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -223,6 +223,7 @@ public final class MediaCodecUtil { && ("CIPAACDecoder".equals(name) || "CIPMP3Decoder".equals(name) || "CIPVorbisDecoder".equals(name) + || "CIPAMRNBDecoder".equals(name) || "AACDecoder".equals(name) || "MP3Decoder".equals(name))) { return false; From 99503e6e3a671160f4bfca078ae20c288ee515d7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Oct 2016 09:55:45 -0700 Subject: [PATCH 047/206] Fix playback of OGG with only a single payload page Issue: #1976 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137044583 --- .../extractor/ogg/DefaultOggSeekerTest.java | 9 +++++---- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 8 ++++---- .../exoplayer2/extractor/ogg/OggTestFile.java | 16 ++++++++++++++-- .../extractor/ogg/DefaultOggSeeker.java | 18 +++++++++++++----- .../exoplayer2/extractor/ogg/StreamReader.java | 5 ++++- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index cccc619a0e..71edba0612 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -27,9 +27,9 @@ import junit.framework.TestCase; */ public final class DefaultOggSeekerTest extends TestCase { - public void testSetupUnboundAudioLength() { + public void testSetupWithUnsetEndPositionFails() { try { - new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader()); + new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader(), 1, 1); fail(); } catch (IllegalArgumentException e) { // ignored @@ -43,11 +43,12 @@ public final class DefaultOggSeekerTest extends TestCase { } } - public void testSeeking(Random random) throws IOException, InterruptedException { + private void testSeeking(Random random) throws IOException, InterruptedException { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); TestStreamReader streamReader = new TestStreamReader(); - DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader); + DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader, + testFile.firstPayloadPageSize, testFile.firstPayloadPageGranulePosition); OggPageHeader pageHeader = new OggPageHeader(); while (true) { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index 052f45b8f4..5431d35bdf 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -29,7 +29,7 @@ import junit.framework.TestCase; public class DefaultOggSeekerUtilMethodsTest extends TestCase { private Random random = new Random(0); - + public void testSkipToNextPage() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( @@ -75,7 +75,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { private static void skipToNextPage(ExtractorInput extractorInput) throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, extractorInput.getLength(), - new FlacReader()); + new FlacReader(), 1, 2); while (true) { try { oggSeeker.skipToNextPage(extractorInput); @@ -143,7 +143,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { private void skipToPageOfGranule(ExtractorInput input, long granule, long elapsedSamplesExpected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader()); + DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1)); @@ -193,7 +193,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader()); + DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index 88f36d35c1..b1294c7a14 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -33,12 +33,17 @@ import junit.framework.Assert; long lastGranule; int packetCount; int pageCount; + int firstPayloadPageSize; + long firstPayloadPageGranulePosition; - private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount) { + private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount, + int firstPayloadPageSize, long firstPayloadPageGranulePosition) { this.data = data; this.lastGranule = lastGranule; this.packetCount = packetCount; this.pageCount = pageCount; + this.firstPayloadPageSize = firstPayloadPageSize; + this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; } public static OggTestFile generate(Random random, int pageCount) { @@ -47,6 +52,8 @@ import junit.framework.Assert; long granule = 0; int packetLength = -1; int packetCount = 0; + int firstPayloadPageSize = 0; + long firstPayloadPageGranulePosition = 0; for (int i = 0; i < pageCount; i++) { int headerType = 0x00; @@ -89,6 +96,10 @@ import junit.framework.Assert; byte[] payload = TestUtil.buildTestData(bodySize, random); fileData.add(payload); fileSize += payload.length; + if (i == 0) { + firstPayloadPageSize = header.length + bodySize; + firstPayloadPageGranulePosition = granule; + } } byte[] file = new byte[fileSize]; @@ -97,7 +108,8 @@ import junit.framework.Assert; System.arraycopy(data, 0, file, position, data.length); position += data.length; } - return new OggTestFile(file, granule, packetCount, pageCount); + return new OggTestFile(file, granule, packetCount, pageCount, firstPayloadPageSize, + firstPayloadPageGranulePosition); } public int findPreviousPageStart(long position) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c6e5d46b8e..87e5811a9a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -59,13 +59,21 @@ import java.io.IOException; * @param startPosition Start position of the payload (inclusive). * @param endPosition End position of the payload (exclusive). * @param streamReader StreamReader instance which owns this OggSeeker + * @param firstPayloadPageSize The total size of the first payload page, in bytes. + * @param firstPayloadPageGranulePosition The granule position of the first payload page. */ - public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader) { + public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader, + int firstPayloadPageSize, long firstPayloadPageGranulePosition) { Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); this.streamReader = streamReader; this.startPosition = startPosition; this.endPosition = endPosition; - this.state = STATE_SEEK_TO_END; + if (firstPayloadPageSize == endPosition - startPosition) { + totalGranules = firstPayloadPageGranulePosition; + state = STATE_IDLE; + } else { + state = STATE_SEEK_TO_END; + } } @Override @@ -77,9 +85,9 @@ import java.io.IOException; positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPagePosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; - if (lastPagePosition > positionBeforeSeekToEnd) { - return lastPagePosition; + long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + if (lastPageSearchPosition > positionBeforeSeekToEnd) { + return lastPageSearchPosition; } // Fall through. case STATE_READ_LAST_PAGE: diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index aa3f8e2353..a8b44f8fe9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -144,7 +144,10 @@ import java.io.IOException; } else if (input.getLength() == C.LENGTH_UNSET) { oggSeeker = new UnseekableOggSeeker(); } else { - oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this); + OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader(); + oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this, + firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, + firstPayloadPageHeader.granulePosition); } setupData = null; From 1b39d21ed41ebaaac47fe4a5f61ba40b77bee1dc Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 24 Oct 2016 19:22:41 +0100 Subject: [PATCH 048/206] Fix indentation and missing javadoc --- .../exoplayer2/extractor/mp4/AtomParsers.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index da29305311..b8456ecb4d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -95,8 +95,8 @@ import java.util.List; Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -407,6 +407,7 @@ import java.util.List; * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. + * @return Parsed metadata, or null. */ public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { @@ -868,12 +869,12 @@ import java.util.List; /** * Parses a stsd atom (defined in 14496-12). * - * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. - * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @param isQuickTime True for QuickTime media. False otherwise. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, From 8caaf0b5d992d5a31e7771ca49ec76ff5232e84b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Oct 2016 23:45:50 +0100 Subject: [PATCH 049/206] Big cleanup of mp4 metadata extraction --- .../android/exoplayer2/demo/EventLogger.java | 39 +- .../exoplayer2/extractor/mp4/Atom.java | 3 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 337 +----------------- .../extractor/mp4/MetadataUtil.java | 323 +++++++++++++++++ .../exoplayer2/metadata/id3/Id3Util.java | 63 ---- .../exoplayer2/util/ParsableByteArray.java | 6 +- 6 files changed, 369 insertions(+), 402 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index c3fc5b9549..5e0b76f68b 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -154,6 +154,18 @@ import java.util.Locale; } Log.d(TAG, " ]"); } + // Log metadata for at most one of the tracks selected for the renderer. + if (trackSelection != null) { + for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { + Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; + if (metadata != null) { + Log.d(TAG, " Metadata ["); + printMetadata(metadata, " "); + Log.d(TAG, " ]"); + break; + } + } + } Log.d(TAG, " ]"); } } @@ -184,7 +196,7 @@ import java.util.Locale; @Override public void onMetadata(Metadata metadata) { Log.d(TAG, "onMetadata ["); - printMetadata(metadata); + printMetadata(metadata, " "); Log.d(TAG, "]"); } @@ -208,13 +220,8 @@ import java.util.Locale; @Override public void onAudioInputFormatChanged(Format format) { - boolean hasMetadata = format.metadata != null; Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) - + (hasMetadata ? "" : "]")); - if (hasMetadata) { - printMetadata(format.metadata); - Log.d(TAG, "]"); - } + + "]"); } @Override @@ -335,35 +342,35 @@ import java.util.Locale; Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } - private void printMetadata(Metadata metadata) { + private void printMetadata(Metadata metadata, String prefix) { for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof TxxxFrame) { TxxxFrame txxxFrame = (TxxxFrame) entry; - Log.d(TAG, String.format(" %s: description=%s, value=%s", txxxFrame.id, + Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, txxxFrame.description, txxxFrame.value)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, String.format(" %s: owner=%s", privFrame.id, privFrame.owner)); + Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, filename=%s, description=%s", + Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, String.format(" %s: mimeType=%s, description=%s", + Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, String.format(" %s: description=%s", textInformationFrame.id, + Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id, textInformationFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, String.format(" %s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); + Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, + commentFrame.language, commentFrame.description, commentFrame.text)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, String.format(" %s", id3Frame.id)); + Log.d(TAG, prefix + String.format("%s", id3Frame.id)); } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index e93e9e3d9c..749c9b3542 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -132,7 +132,6 @@ import java.util.List; public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); - public static final int TYPE_DASHES = Util.getIntegerCodeForString("----"); public final int type; @@ -299,7 +298,7 @@ import java.util.List; * @return The corresponding four character string. */ public static String getAtomTypeString(int type) { - return "" + (char) (type >> 24) + return "" + (char) ((type >> 24) & 0xFF) + (char) ((type >> 16) & 0xFF) + (char) ((type >> 8) & 0xFF) + (char) (type & 0xFF); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index b8456ecb4d..47cb3262e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.BinaryFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.Id3Util; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -418,336 +413,45 @@ import java.util.List; ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { + int atomPosition = udtaData.getPosition(); int atomSize = udtaData.readInt(); int atomType = udtaData.readInt(); if (atomType == Atom.TYPE_meta) { - udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); - udtaData.setLimit(udtaData.getPosition() + atomSize); - return parseMetaAtom(udtaData); + udtaData.setPosition(atomPosition); + return parseMetaAtom(udtaData, atomPosition + atomSize); } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray data) { - data.skipBytes(Atom.FULL_HEADER_SIZE); - ParsableByteArray ilst = new ParsableByteArray(); - while (data.bytesLeft() >= Atom.HEADER_SIZE) { - int payloadSize = data.readInt() - Atom.HEADER_SIZE; - int atomType = data.readInt(); + private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) { + meta.skipBytes(Atom.FULL_HEADER_SIZE); + while (meta.getPosition() < limit) { + int atomPosition = meta.getPosition(); + int atomSize = meta.readInt(); + int atomType = meta.readInt(); if (atomType == Atom.TYPE_ilst) { - ilst.reset(data.data, data.getPosition() + payloadSize); - ilst.setPosition(data.getPosition()); - Metadata metadata = parseIlst(ilst); - if (metadata != null) { - return metadata; - } + meta.setPosition(atomPosition); + return parseIlst(meta, atomPosition + atomSize); } - data.skipBytes(payloadSize); + meta.skipBytes(atomSize - Atom.HEADER_SIZE); } return null; } - private static Metadata parseIlst(ParsableByteArray ilst) { + private static Metadata parseIlst(ParsableByteArray ilst, int limit) { + ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); - while (ilst.bytesLeft() > 0) { - int position = ilst.getPosition(); - int endPosition = position + ilst.readInt(); - int type = ilst.readInt(); - parseIlstElement(ilst, type, endPosition, entries); - ilst.setPosition(endPosition); + while (ilst.getPosition() < limit) { + Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + if (entry != null) { + entries.add(entry); + } } return entries.isEmpty() ? null : new Metadata(entries); } - private static final String P1 = "\u00a9"; - private static final String P2 = "\ufffd"; - private static final int TYPE_NAME_1 = Util.getIntegerCodeForString(P1 + "nam"); - private static final int TYPE_NAME_2 = Util.getIntegerCodeForString(P2 + "nam"); - private static final int TYPE_NAME_3 = Util.getIntegerCodeForString(P1 + "trk"); - private static final int TYPE_NAME_4 = Util.getIntegerCodeForString(P2 + "trk"); - private static final int TYPE_COMMENT_1 = Util.getIntegerCodeForString(P1 + "cmt"); - private static final int TYPE_COMMENT_2 = Util.getIntegerCodeForString(P2 + "cmt"); - private static final int TYPE_YEAR_1 = Util.getIntegerCodeForString(P1 + "day"); - private static final int TYPE_YEAR_2 = Util.getIntegerCodeForString(P2 + "day"); - private static final int TYPE_ARTIST_1 = Util.getIntegerCodeForString(P1 + "ART"); - private static final int TYPE_ARTIST_2 = Util.getIntegerCodeForString(P2 + "ART"); - private static final int TYPE_ENCODER_1 = Util.getIntegerCodeForString(P1 + "too"); - private static final int TYPE_ENCODER_2 = Util.getIntegerCodeForString(P2 + "too"); - private static final int TYPE_ALBUM_1 = Util.getIntegerCodeForString(P1 + "alb"); - private static final int TYPE_ALBUM_2 = Util.getIntegerCodeForString(P2 + "alb"); - private static final int TYPE_COMPOSER_1 = Util.getIntegerCodeForString(P1 + "com"); - private static final int TYPE_COMPOSER_2 = Util.getIntegerCodeForString(P2 + "com"); - private static final int TYPE_COMPOSER_3 = Util.getIntegerCodeForString(P1 + "wrt"); - private static final int TYPE_COMPOSER_4 = Util.getIntegerCodeForString(P2 + "wrt"); - private static final int TYPE_LYRICS_1 = Util.getIntegerCodeForString(P1 + "lyr"); - private static final int TYPE_LYRICS_2 = Util.getIntegerCodeForString(P2 + "lyr"); - private static final int TYPE_GENRE_1 = Util.getIntegerCodeForString(P1 + "gen"); - private static final int TYPE_GENRE_2 = Util.getIntegerCodeForString(P2 + "gen"); - private static final int TYPE_STANDARD_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING_1 = Util.getIntegerCodeForString(P1 + "grp"); - private static final int TYPE_GROUPING_2 = Util.getIntegerCodeForString(P2 + "grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); - private static final int TYPE_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_SHOW = Util.getIntegerCodeForString("tvsh"); - - // TBD: covr = cover art, various account and iTunes specific attributes, more TV attributes - - private static void parseIlstElement(ParsableByteArray ilst, int type, int endPosition, - List builder) { - if (type == TYPE_NAME_1 || type == TYPE_NAME_2 || type == TYPE_NAME_3 || type == TYPE_NAME_4) { - parseTextAttribute(builder, "TIT2", ilst); - } else if (type == TYPE_COMMENT_1 || type == TYPE_COMMENT_2) { - parseCommentAttribute(builder, "COMM", ilst); - } else if (type == TYPE_YEAR_1 || type == TYPE_YEAR_2) { - parseTextAttribute(builder, "TDRC", ilst); - } else if (type == TYPE_ARTIST_1 || type == TYPE_ARTIST_2) { - parseTextAttribute(builder, "TPE1", ilst); - } else if (type == TYPE_ENCODER_1 || type == TYPE_ENCODER_2) { - parseTextAttribute(builder, "TSSE", ilst); - } else if (type == TYPE_ALBUM_1 || type == TYPE_ALBUM_2) { - parseTextAttribute(builder, "TALB", ilst); - } else if (type == TYPE_COMPOSER_1 || type == TYPE_COMPOSER_2 || - type == TYPE_COMPOSER_3 || type == TYPE_COMPOSER_4) { - parseTextAttribute(builder, "TCOM", ilst); - } else if (type == TYPE_LYRICS_1 || type == TYPE_LYRICS_2) { - parseTextAttribute(builder, "lyrics", ilst); - } else if (type == TYPE_STANDARD_GENRE) { - parseStandardGenreAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GENRE_1 || type == TYPE_GENRE_2) { - parseTextAttribute(builder, "TCON", ilst); - } else if (type == TYPE_GROUPING_1 || type == TYPE_GROUPING_2) { - parseTextAttribute(builder, "TIT1", ilst); - } else if (type == TYPE_DISK_NUMBER) { - parseIndexAndCountAttribute(builder, "TPOS", ilst, endPosition); - } else if (type == TYPE_TRACK_NUMBER) { - parseIndexAndCountAttribute(builder, "TRCK", ilst, endPosition); - } else if (type == TYPE_TEMPO) { - parseIntegerAttribute(builder, "TBPM", ilst); - } else if (type == TYPE_COMPILATION) { - parseBooleanAttribute(builder, "TCMP", ilst); - } else if (type == TYPE_ALBUM_ARTIST) { - parseTextAttribute(builder, "TPE2", ilst); - } else if (type == TYPE_SORT_TRACK_NAME) { - parseTextAttribute(builder, "TSOT", ilst); - } else if (type == TYPE_SORT_ALBUM) { - parseTextAttribute(builder, "TSO2", ilst); - } else if (type == TYPE_SORT_ARTIST) { - parseTextAttribute(builder, "TSOA", ilst); - } else if (type == TYPE_SORT_ALBUM_ARTIST) { - parseTextAttribute(builder, "TSOP", ilst); - } else if (type == TYPE_SORT_COMPOSER) { - parseTextAttribute(builder, "TSOC", ilst); - } else if (type == TYPE_SORT_SHOW) { - parseTextAttribute(builder, "sortShow", ilst); - } else if (type == TYPE_GAPLESS_ALBUM) { - parseBooleanAttribute(builder, "gaplessAlbum", ilst); - } else if (type == TYPE_SHOW) { - parseTextAttribute(builder, "show", ilst); - } else if (type == Atom.TYPE_DASHES) { - parseExtendedAttribute(builder, ilst, endPosition); - } - } - - private static void parseTextAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new TextInformationFrame(attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseCommentAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - ilst.skipBytes(4); - String value = ilst.readNullTerminatedString(length - 4); - Id3Frame frame = new CommentFrame("eng", attributeName, value); - builder.add(frame); - } else { - ilst.skipBytes(length); - } - } - - private static void parseBooleanAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = n == 0 ? "0" : "1"; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIntegerAttribute(List builder, String attributeName, - ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof Integer) { - int n = (Integer) value; - String s = "" + n; - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseIndexAndCountAttribute(List builder, - String attributeName, ParsableByteArray ilst, int endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 8) { - int index = (bytes[2] << 8) + (bytes[3] & 0xFF); - int count = (bytes[4] << 8) + (bytes[5] & 0xFF); - if (index > 0) { - String s = "" + index; - if (count > 0) { - s = s + "/" + count; - } - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseStandardGenreAttribute(List builder, - String attributeName, ParsableByteArray ilst) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_data) { - Object value = parseDataBox(ilst, length); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - if (bytes.length == 2) { - int code = (bytes[0] << 8) + (bytes[1] & 0xFF); - String s = Id3Util.decodeGenre(code); - if (s != null) { - Id3Frame frame = new TextInformationFrame(attributeName, s); - builder.add(frame); - } - } - } - } else { - ilst.skipBytes(length); - } - } - - private static void parseExtendedAttribute(List builder, ParsableByteArray ilst, - int endPosition) { - String domain = null; - String name = null; - Object value = null; - - while (ilst.getPosition() < endPosition) { - int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; - int key = ilst.readInt(); - ilst.skipBytes(4); - if (key == Atom.TYPE_mean) { - domain = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_name) { - name = ilst.readNullTerminatedString(length); - } else if (key == Atom.TYPE_data) { - value = parseDataBox(ilst, length); - } else { - ilst.skipBytes(length); - } - } - - if (value != null) { - if (Util.areEqual(domain, "com.apple.iTunes")) { - String s = new String((byte[]) value); - Id3Frame frame = new CommentFrame("eng", name, s); - builder.add(frame); - } else if (domain != null && name != null) { - String extendedName = domain + "." + name; - if (value instanceof String) { - Id3Frame frame = new TextInformationFrame(extendedName, (String) value); - builder.add(frame); - } else if (value instanceof Integer) { - Id3Frame frame = new TextInformationFrame(extendedName, value.toString()); - builder.add(frame); - } else if (value instanceof byte[]) { - byte[] bb = (byte[]) value; - Id3Frame frame = new BinaryFrame(extendedName, bb); - builder.add(frame); - } - } - } - } - - private static Object parseDataBox(ParsableByteArray ilst, int length) { - int versionAndFlags = ilst.readInt(); - int flags = versionAndFlags & 0xFFFFFF; - boolean isText = (flags == 1); - boolean isData = (flags == 0); - boolean isImageData = (flags == 0xD); - boolean isInteger = (flags == 21); - int dataLength = length - 4; - if (isText) { - return ilst.readNullTerminatedString(dataLength); - } else if (isInteger) { - if (dataLength == 1) { - return ilst.readUnsignedByte(); - } else if (dataLength == 2) { - return ilst.readUnsignedShort(); - } else { - ilst.skipBytes(dataLength); - return null; - } - } else if (isData) { - byte[] bytes = new byte[dataLength]; - ilst.readBytes(bytes, 0, dataLength); - return bytes; - } else { - ilst.skipBytes(dataLength); - return null; - } - } - /** * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. * @@ -756,12 +460,9 @@ import java.util.List; */ private static long parseMvhd(ParsableByteArray mvhd) { mvhd.setPosition(Atom.HEADER_SIZE); - int fullAtom = mvhd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); - mvhd.skipBytes(version == 0 ? 8 : 16); - return mvhd.readUnsignedInt(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java new file mode 100644 index 0000000000..4bfef85d10 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2016 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.extractor.mp4; + +import android.util.Log; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +/** + * Parses metadata items stored in ilst atoms. + */ +/* package */ final class MetadataUtil { + + private static final String TAG = "MetadataUtil"; + + // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. + private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); + private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); + private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); + private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); + private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); + private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); + private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); + private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); + private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); + private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); + private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + + // Codes that have equivalent ID3 frames. + private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); + private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); + private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); + private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); + private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); + private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); + private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); + private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); + private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); + private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); + private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); + private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); + private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + + // Types that do not have equivalent ID3 frames. + private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); + private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); + private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); + private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + + // Type for items that are intended for internal use by the player. + private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + + // Standard genres. + private static final String[] STANDARD_GENRES = new String[] { + // These are the official ID3v1 genres. + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", + "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", + "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", + "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", + "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Hard Rock", + // These were made up by the authors of Winamp but backported into the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", + "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", + "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", + "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", + "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", + "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", + "Dance Hall", + // These were also invented by the Winamp folks but ignored by the ID3 authors. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", + "Synthpop" + }; + + private static final String LANGUAGE_UNDEFINED = "und"; + + private MetadataUtil() {} + + /** + * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting + * from the current position of the {@link ParsableByteArray}, and the position is advanced by + * the size of the element. The position is advanced even if the element's type is unrecognized. + * + * @param ilst Holds the data to be parsed. + * @return The parsed element, or null if the element's type was not recognized. + */ + public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + int position = ilst.getPosition(); + int endPosition = position + ilst.readInt(); + int type = ilst.readInt(); + int typeTopByte = (type >> 24) & 0xFF; + try { + if (typeTopByte == '\u00A9' /* Copyright char */ + || typeTopByte == '\uFFFD' /* Replacement char */) { + int shortType = type & 0x00FFFFFF; + if (shortType == SHORT_TYPE_COMMENT) { + return parseCommentAttribute(type, ilst); + } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) { + return parseTextAttribute(type, "TIT2", ilst); + } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) { + return parseTextAttribute(type, "TCOM", ilst); + } else if (shortType == SHORT_TYPE_YEAR) { + return parseTextAttribute(type, "TDRC", ilst); + } else if (shortType == SHORT_TYPE_ARTIST) { + return parseTextAttribute(type, "TPE1", ilst); + } else if (shortType == SHORT_TYPE_ENCODER) { + return parseTextAttribute(type, "TSSE", ilst); + } else if (shortType == SHORT_TYPE_ALBUM) { + return parseTextAttribute(type, "TALB", ilst); + } else if (shortType == SHORT_TYPE_LYRICS) { + return parseTextAttribute(type, "USLT", ilst); + } else if (shortType == SHORT_TYPE_GENRE) { + return parseTextAttribute(type, "TCON", ilst); + } else if (shortType == TYPE_GROUPING) { + return parseTextAttribute(type, "TIT1", ilst); + } + } else if (type == TYPE_GENRE) { + return parseStandardGenreAttribute(ilst); + } else if (type == TYPE_DISK_NUMBER) { + return parseIndexAndCountAttribute(type, "TPOS", ilst); + } else if (type == TYPE_TRACK_NUMBER) { + return parseIndexAndCountAttribute(type, "TRCK", ilst); + } else if (type == TYPE_TEMPO) { + return parseUint8Attribute(type, "TBPM", ilst, true, false); + } else if (type == TYPE_COMPILATION) { + return parseUint8Attribute(type, "TCMP", ilst, true, true); + } else if (type == TYPE_COVER_ART) { + return parseCoverArt(ilst); + } else if (type == TYPE_ALBUM_ARTIST) { + return parseTextAttribute(type, "TPE2", ilst); + } else if (type == TYPE_SORT_TRACK_NAME) { + return parseTextAttribute(type, "TSOT", ilst); + } else if (type == TYPE_SORT_ALBUM) { + return parseTextAttribute(type, "TSO2", ilst); + } else if (type == TYPE_SORT_ARTIST) { + return parseTextAttribute(type, "TSOA", ilst); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + return parseTextAttribute(type, "TSOP", ilst); + } else if (type == TYPE_SORT_COMPOSER) { + return parseTextAttribute(type, "TSOC", ilst); + } else if (type == TYPE_RATING) { + return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false); + } else if (type == TYPE_GAPLESS_ALBUM) { + return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true); + } else if (type == TYPE_TV_SORT_SHOW) { + return parseTextAttribute(type, "TVSHOWSORT", ilst); + } else if (type == TYPE_TV_SHOW) { + return parseTextAttribute(type, "TVSHOW", ilst); + } else if (type == TYPE_INTERNAL) { + return parseInternalAttribute(ilst, endPosition); + } + Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type)); + return null; + } finally { + ilst.setPosition(endPosition); + } + } + + private static TextInformationFrame parseTextAttribute(int type, String id, + ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new TextInformationFrame(id, value); + } + Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new CommentFrame(LANGUAGE_UNDEFINED, value, value); + } + Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data, + boolean isTextInformationFrame, boolean isBoolean) { + int value = parseUint8AttributeValue(data); + if (isBoolean) { + value = Math.min(1, value); + } + if (value >= 0) { + return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value)) + : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); + } + Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName, + ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data && atomSize >= 22) { + data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) + int index = data.readUnsignedShort(); + if (index > 0) { + String description = "" + index; + int count = data.readUnsignedShort(); + if (count > 0) { + description += "/" + count; + } + return new TextInformationFrame(attributeName, description); + } + } + Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { + int genreCode = parseUint8AttributeValue(data); + String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] : null; + if (genreString != null) { + return new TextInformationFrame("TCON", genreString); + } + Log.w(TAG, "Failed to parse standard genre code"); + return null; + } + + private static ApicFrame parseCoverArt(ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + int fullVersionInt = data.readInt(); + int flags = Atom.parseFullAtomFlags(fullVersionInt); + String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + if (mimeType == null) { + Log.w(TAG, "Unrecognized cover art flags: " + flags); + return null; + } + data.skipBytes(4); // empty (4) + byte[] pictureData = new byte[atomSize - 16]; + data.readBytes(pictureData, 0, pictureData.length); + return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData); + } + Log.w(TAG, "Failed to parse cover art attribute"); + return null; + } + + private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { + String domain = null; + String name = null; + int dataAtomPosition = -1; + int dataAtomSize = -1; + while (data.getPosition() < endPosition) { + int atomPosition = data.getPosition(); + int atomSize = data.readInt(); + int atomType = data.readInt(); + data.skipBytes(4); // version (1), flags (3) + if (atomType == Atom.TYPE_mean) { + domain = data.readNullTerminatedString(atomSize - 12); + } else if (atomType == Atom.TYPE_name) { + name = data.readNullTerminatedString(atomSize - 12); + } else { + if (atomType == Atom.TYPE_data) { + dataAtomPosition = atomPosition; + dataAtomSize = atomSize; + } + data.skipBytes(atomSize - 12); + } + } + if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { + // We're only interested in iTunSMPB. + return null; + } + data.setPosition(dataAtomPosition); + data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(dataAtomSize - 16); + return new CommentFrame(LANGUAGE_UNDEFINED, name, value); + } + + private static int parseUint8AttributeValue(ParsableByteArray data) { + data.skipBytes(4); // atomSize + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + return data.readUnsignedByte(); + } + Log.w(TAG, "Failed to parse uint8 attribute value"); + return -1; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java deleted file mode 100644 index 64f2ce9908..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Util.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2016 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; - -/** - * ID3 utility methods. - */ -public final class Id3Util { - - private static final String[] STANDARD_GENRES = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", - "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", - "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", - "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", - "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", - "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", - "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", - "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", - "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", - "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", - "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", - "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", - "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", - "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", - "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" - }; - - private Id3Util() {} - - public static String decodeGenre(int code) { - return (0 < code && code <= STANDARD_GENRES.length) ? STANDARD_GENRES[code - 1] : null; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 7691899ade..05c29ca032 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -300,9 +300,9 @@ public final class ParsableByteArray { */ public int readLittleEndianInt() { return (data[position++] & 0xFF) - | (data[position++] & 0xFF) << 8 - | (data[position++] & 0xFF) << 16 - | (data[position++] & 0xFF) << 24; + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 24; } /** From ba6368a07a36109fc3fa027705e6ab40258f8144 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 21 Jul 2016 02:08:21 +0100 Subject: [PATCH 050/206] Don't try to access the caption manager when in edit mode. Issue: #1991 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137131819 --- .../android/exoplayer2/trackselection/TrackSelector.java | 1 + .../com/google/android/exoplayer2/ui/SubtitleView.java | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index 41c62f6e0e..9c859312cb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -46,6 +46,7 @@ public abstract class TrackSelector { * @param trackSelections The new track selections. */ void onTrackSelectionsChanged(TrackSelections trackSelections); + } private final Handler eventHandler; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 0c8d9ef92e..49516ab6f4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -122,10 +122,10 @@ public final class SubtitleView extends View implements TextRenderer.Output { /** * Sets the text size to one derived from {@link CaptioningManager#getFontScale()}, or to a - * default size on API level 19 and earlier. + * default size before API level 19. */ public void setUserDefaultTextSize() { - float fontScale = Util.SDK_INT >= 19 ? getUserCaptionFontScaleV19() : 1f; + float fontScale = Util.SDK_INT >= 19 && !isInEditMode() ? getUserCaptionFontScaleV19() : 1f; setFractionalTextSize(DEFAULT_TEXT_SIZE_FRACTION * fontScale); } @@ -180,10 +180,11 @@ public final class SubtitleView extends View implements TextRenderer.Output { /** * Sets the caption style to be equivalent to the one returned by - * {@link CaptioningManager#getUserStyle()}, or to a default style on API level 19 and earlier. + * {@link CaptioningManager#getUserStyle()}, or to a default style before API level 19. */ public void setUserDefaultStyle() { - setStyle(Util.SDK_INT >= 19 ? getUserCaptionStyleV19() : CaptionStyleCompat.DEFAULT); + setStyle(Util.SDK_INT >= 19 && !isInEditMode() + ? getUserCaptionStyleV19() : CaptionStyleCompat.DEFAULT); } /** From 8b3025d450278572a3e0f1da09fe039d7b918678 Mon Sep 17 00:00:00 2001 From: "[]inger" <[]inger@google.com> Date: Tue, 25 Oct 2016 05:10:42 -0700 Subject: [PATCH 051/206] Make control view layout resource customizable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137145209 --- .../exoplayer2/ui/PlaybackControlView.java | 138 ++++++++++++------ .../exoplayer2/ui/SimpleExoPlayerView.java | 21 ++- .../res/layout/exo_playback_control_view.xml | 27 ++-- .../res/layout/exo_simple_player_view.xml | 14 +- library/src/main/res/values/attrs.xml | 3 + library/src/main/res/values/ids.xml | 26 ++++ library/src/main/res/values/styles.xml | 10 ++ 7 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 library/src/main/res/values/ids.xml diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 89c778d072..470e173c02 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -24,7 +24,6 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; -import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; @@ -38,6 +37,16 @@ import java.util.Locale; /** * A view to control video playback of an {@link ExoPlayer}. + * + * By setting the view attribute {@code controller_layout_id} a layout resource to use can + * be customized. All views are optional but if the buttons should have an appropriate logic + * assigned, the id of the views in the layout have to match the expected ids as follows: + * + *

    + *
  • Playback: {@code exo_play}, {@code exo_pause}, {@code exo_ffwd}, {@code exo_rew}.
  • + *
  • Progress: {@code exo_progress}, {@code exo_time_current}, {@code exo_time}.
  • + *
  • Playlist navigation: {@code exo_previous}, {@code exo_next}.
  • + *
*/ public class PlaybackControlView extends FrameLayout { @@ -63,12 +72,13 @@ public class PlaybackControlView extends FrameLayout { private final ComponentListener componentListener; private final View previousButton; private final View nextButton; - private final ImageButton playButton; + private final View playButton; + private final View pauseButton; + private final View fastForwardButton; + private final View rewindButton; private final TextView time; private final TextView timeCurrent; private final SeekBar progressBar; - private final View fastForwardButton; - private final View rewindButton; private final StringBuilder formatBuilder; private final Formatter formatter; private final Timeline.Window currentWindow; @@ -108,6 +118,7 @@ public class PlaybackControlView extends FrameLayout { public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + int layoutResourceId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; @@ -119,32 +130,49 @@ public class PlaybackControlView extends FrameLayout { fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); + layoutResourceId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, + layoutResourceId); } finally { a.recycle(); } } - currentWindow = new Timeline.Window(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); componentListener = new ComponentListener(); - LayoutInflater.from(context).inflate(R.layout.exo_playback_control_view, this); - time = (TextView) findViewById(R.id.time); - timeCurrent = (TextView) findViewById(R.id.time_current); - progressBar = (SeekBar) findViewById(R.id.mediacontroller_progress); - progressBar.setOnSeekBarChangeListener(componentListener); - progressBar.setMax(PROGRESS_BAR_MAX); - playButton = (ImageButton) findViewById(R.id.play); - playButton.setOnClickListener(componentListener); - previousButton = findViewById(R.id.prev); - previousButton.setOnClickListener(componentListener); - nextButton = findViewById(R.id.next); - nextButton.setOnClickListener(componentListener); - rewindButton = findViewById(R.id.rew); - rewindButton.setOnClickListener(componentListener); - fastForwardButton = findViewById(R.id.ffwd); - fastForwardButton.setOnClickListener(componentListener); + LayoutInflater.from(context).inflate(layoutResourceId, this); + time = (TextView) findViewById(R.id.exo_time); + timeCurrent = (TextView) findViewById(R.id.exo_time_current); + progressBar = (SeekBar) findViewById(R.id.exo_progress); + if (progressBar != null) { + progressBar.setOnSeekBarChangeListener(componentListener); + progressBar.setMax(PROGRESS_BAR_MAX); + } + playButton = findViewById(R.id.exo_play); + if (playButton != null) { + playButton.setOnClickListener(componentListener); + } + pauseButton = findViewById(R.id.exo_pause); + if (pauseButton != null) { + pauseButton.setOnClickListener(componentListener); + } + previousButton = findViewById(R.id.exo_prev); + if (previousButton != null) { + previousButton.setOnClickListener(componentListener); + } + nextButton = findViewById(R.id.exo_next); + if (nextButton != null) { + nextButton.setOnClickListener(componentListener); + } + rewindButton = findViewById(R.id.exo_rew); + if (rewindButton != null) { + rewindButton.setOnClickListener(componentListener); + } + fastForwardButton = findViewById(R.id.exo_ffwd); + if (fastForwardButton != null) { + fastForwardButton.setOnClickListener(componentListener); + } } /** @@ -285,11 +313,12 @@ public class PlaybackControlView extends FrameLayout { return; } boolean playing = player != null && player.getPlayWhenReady(); - String contentDescription = getResources().getString( - playing ? R.string.exo_controls_pause_description : R.string.exo_controls_play_description); - playButton.setContentDescription(contentDescription); - playButton.setImageResource( - playing ? R.drawable.exo_controls_pause : R.drawable.exo_controls_play); + if (playButton != null) { + playButton.setVisibility(playing ? GONE : VISIBLE); + } + if (pauseButton != null) { + pauseButton.setVisibility(playing ? VISIBLE : GONE); + } } private void updateNavigation() { @@ -313,7 +342,9 @@ public class PlaybackControlView extends FrameLayout { setButtonEnabled(enableNext, nextButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - progressBar.setEnabled(isSeekable); + if (progressBar != null) { + progressBar.setEnabled(isSeekable); + } } private void updateProgress() { @@ -322,16 +353,21 @@ public class PlaybackControlView extends FrameLayout { } long duration = player == null ? 0 : player.getDuration(); long position = player == null ? 0 : player.getCurrentPosition(); - time.setText(stringForTime(duration)); - if (!dragging) { + if (time != null) { + time.setText(stringForTime(duration)); + } + if (timeCurrent != null && !dragging) { timeCurrent.setText(stringForTime(position)); } - if (!dragging) { - progressBar.setProgress(progressBarValue(position)); + + if (progressBar != null) { + if (!dragging) { + progressBar.setProgress(progressBarValue(position)); + } + long bufferedPosition = player == null ? 0 : player.getBufferedPosition(); + progressBar.setSecondaryProgress(progressBarValue(bufferedPosition)); + // Remove scheduled updates. } - long bufferedPosition = player == null ? 0 : player.getBufferedPosition(); - progressBar.setSecondaryProgress(progressBarValue(bufferedPosition)); - // Remove scheduled updates. removeCallbacks(updateProgressAction); // Schedule an update if necessary. int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState(); @@ -350,6 +386,9 @@ public class PlaybackControlView extends FrameLayout { } private void setButtonEnabled(boolean enabled, View view) { + if (view == null) { + return; + } view.setEnabled(enabled); if (Util.SDK_INT >= 11) { setViewAlphaV11(view, enabled ? 1f : 0.3f); @@ -500,7 +539,7 @@ public class PlaybackControlView extends FrameLayout { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { + if (fromUser && timeCurrent != null) { timeCurrent.setText(stringForTime(positionValue(progress))); } } @@ -508,7 +547,9 @@ public class PlaybackControlView extends FrameLayout { @Override public void onStopTrackingTouch(SeekBar seekBar) { dragging = false; - player.seekTo(positionValue(seekBar.getProgress())); + if (player != null) { + player.seekTo(positionValue(seekBar.getProgress())); + } hideAfterTimeout(); } @@ -542,17 +583,20 @@ public class PlaybackControlView extends FrameLayout { @Override public void onClick(View view) { - Timeline currentTimeline = player.getCurrentTimeline(); - if (nextButton == view) { - next(); - } else if (previousButton == view) { - previous(); - } else if (fastForwardButton == view) { - fastForward(); - } else if (rewindButton == view && currentTimeline != null) { - rewind(); - } else if (playButton == view) { - player.setPlayWhenReady(!player.getPlayWhenReady()); + if (player != null) { + if (nextButton == view) { + next(); + } else if (previousButton == view) { + previous(); + } else if (fastForwardButton == view) { + fastForward(); + } else if (rewindButton == view) { + rewind(); + } else if (playButton == view) { + player.setPlayWhenReady(true); + } else if (pauseButton == view) { + player.setPlayWhenReady(false); + } } hideAfterTimeout(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 51955ccef3..692ff70ce1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -90,23 +90,30 @@ public final class SimpleExoPlayerView extends FrameLayout { LayoutInflater.from(context).inflate(R.layout.exo_simple_player_view, this); componentListener = new ComponentListener(); - layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame); + layout = (AspectRatioFrameLayout) findViewById(R.id.exo_video_frame); layout.setResizeMode(resizeMode); - shutterView = findViewById(R.id.shutter); - subtitleLayout = (SubtitleView) findViewById(R.id.subtitles); + shutterView = findViewById(R.id.exo_shutter); + subtitleLayout = (SubtitleView) findViewById(R.id.exo_subtitles); subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultTextSize(); - controller = (PlaybackControlView) findViewById(R.id.control); - controller.hide(); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + + controller = new PlaybackControlView(context, attrs); controller.setRewindIncrementMs(rewindMs); controller.setFastForwardIncrementMs(fastForwardMs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + controller.hide(); this.controllerShowTimeoutMs = controllerShowTimeoutMs; + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + View view = useTextureView ? new TextureView(context) : new SurfaceView(context); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); view.setLayoutParams(params); surfaceView = view; layout.addView(surfaceView, 0); diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index 5c9d2aeeb9..f9de461b0c 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -14,7 +14,6 @@ limitations under the License. --> - - - + - + + - @@ -56,7 +53,7 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - - - - - + - - diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index d58882c0aa..1cc22314c0 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -23,6 +23,7 @@ + @@ -31,6 +32,7 @@ + @@ -41,6 +43,7 @@ + diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml new file mode 100644 index 0000000000..9e810f1c2b --- /dev/null +++ b/library/src/main/res/values/ids.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml index fe1e26e967..3a1e73f887 100644 --- a/library/src/main/res/values/styles.xml +++ b/library/src/main/res/values/styles.xml @@ -41,4 +41,14 @@ @string/exo_controls_rewind_description + + + + From 1809836c2171b4452f57ab938fcc35d6d4826f38 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 25 Oct 2016 06:37:41 -0700 Subject: [PATCH 052/206] Provide a method for creating a reader for a particular PID This allows the user to create section readers(usually) for reserved pids, like SDT, EIT, CAT, etc. Issue:#726 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137150853 --- .../androidTest/assets/ts/sample_with_sdt.ts | Bin 0 -> 4096 bytes .../extractor/ts/TsExtractorTest.java | 103 +++++++++++++++--- .../ts/DefaultTsPayloadReaderFactory.java | 6 + .../exoplayer2/extractor/ts/TsExtractor.java | 6 + .../extractor/ts/TsPayloadReader.java | 10 ++ 5 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 library/src/androidTest/assets/ts/sample_with_sdt.ts diff --git a/library/src/androidTest/assets/ts/sample_with_sdt.ts b/library/src/androidTest/assets/ts/sample_with_sdt.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d1eccee156c0cbf01b1fd557dfcd843baedc11a GIT binary patch literal 4096 zcmd7VXHXN&x(D#klqy|%lU}745e+T$PyI zQWX-4^iUNP5J40LPwttyGxvNupUyq|VP|$`XXiJ&GtYk?6CgVqz~Jz@no&Ohz<%uy z#P@fMu&E#wi~T(WOf)$3UPOQoER^}5D?@)zBoYQskoP|M^WQK`fB-fCpoQh1c8Ko@ zU|6XF`riQiHw-2aik-TJ2jWY)ce2ePfck%?ShDRQzl2`jX*pSf!B#Bj+ixo z{qoyUjCc*}azcFLPZr3WA!Oa~aLt6umR-B^3|Bi^4!6KuyF+TcZEwZ7vtKqFlij`Z zZuif(CXY!%=+;<)67Z&VZAO{Yxo9{21;8JjJ^< zG3KQas0tHj37O17cj=cKQ-!yO{MI&8{mNfkulh(k^@T0KEfR~k>qd=V&7-~K;XE3a z9FQ`KoueMr%h|t9NQ6r6jpyj&lU(1Tm3G^(gAyzO9Qy52E^g-4Ngo7J8_zkrB?P(g za7@#O@hZNNpyAiw?HAU4`+fibG`~J+iXUMMX>Htj<_t-}?xP-V4|BxI-S8Q?ph~z; zLdQ(x(38>Z#FuA5_Fdk^8@g75M;oP7k`kvfi0BJz%S%=9qJA46Fob*IzH3{%QnV#< z#P{osO5NJ!ci%GvR)Ic+`$W|dbbk4VUUmf1d^QT(dZ(Z$h?LsZ&T95*#EEJqCA_}KP*K~>V?xxLdcAk|p`shZC zDh3&JxU>dfA~aIdRtTdAn7~!b2p86v7@8yp^tZh$dAFh#X>Z?^Zcez#afrH8)oP(?H#Vc%7j9gJ z$XBI?nk4Ccru}|xik&L~q}*l0g&mf5cmWM#f60nkg=mwZ^0}frdRv`L@#OLKmy?kx z=PJ$<&^*jc{oyD*v0^&d`O^gV9tNZenv!!b4cL^yafMVyip`T3Cm4sah z7r&~bS6waN96Bz*K0&l$X?uO{6?_^z9r2fohbB{SvT~}cE?xDY5Lok6*@~Sv0$+x+ z4>D^^>taGG8v%KKB!azK%yxc$HnE9lH<}lcNmc(osy1i9KL`wI;(yAb*y4>U3>O9$ zU0&%R;lXLH$R&-gb%o9zFp=+QPs$>8d;v>3$~F#XJdBeY)j8HaF?y{|G{;W0GK*D4 z?Vi{o5EZj{5dZ*i6C5QQ`G;b^T9o^P>~OiE;?jVVWKv@4(*#F1F8OH-^Y}7c_SJ-k zI8$ z_JL}&X%nY?auM>tCsflycAzNEq^e^28l8jcmcFG=j^LPgfv00}|0^d!pXo?Ob&8!Q z3*V&iyiR0GJ}$7ZW1a@i8uOdxx%oM9Z&ikS^q@rwXKW?DF4^FA-k{9p-TR9s;R@fu z{Fis%%Fk}xA7E5B@G@xEgyOQCq|}m%b94*xYGzt+vQhqdgt{E2;9ra?7CCI2I^3=@ zm*=9h*_3QqE2=y&Zu=hLn;^1Yi=Sjtty(s{Q|K~7PkP$t6wn!8NcO`NzaR!Uf7ekd zxnra_X8QW&l~cV3Az5;je0mm-<27YBz@{D56uY(`;6IZueFLf?oH|qM=470VqJvMa z`CUMtzD?pnj*H8Lfw@9JKHaNzf`+OP(uq@R4$fMaI>F<*BBKg(^d$O9t6l5dnl84Q zS>}oLWRuRnW>bnT(*x@V|0kQSatd%(>Whiu;g8`4s6s&&B5id@&mA0k zTJUZ1N)`cJe+FgnFo_O;9@4R;o$Eel@KlMnv_b=AIPque|HZ04sUd~M`2a7e{V0)vsZ*+Ua*Mer{GEE+gX(qV1wn+43H0CNVy<*L*fcl{ezL zxezhfF#URjw}^Q=HfRiaa_g%Ws=;3Y-5T}MZZYaPm!&|KeM(cqxU-ow)~iVzP3VxCcDrNPxIIWB$k*HL8os=%FEjw`&u>6; zA*mq8AxHTmyl}7tVgrkLg#q@oL>qO0s!I?=-r=XE_C3!U%iXY@cX|GVrkgTn+SD6D z)bC?}*Egnf)##L#tDB0QeSL=C{(xP2cqYN^IhbQ6-ZdM=kdK|8oux%|#3{N$MiC3Z z1Zl0@!1Ir9Pmc37-_%)a38Y#W;l3EzMLSVDN8@_YkN7EeLEpKx2kp)Ge{;{XH^1F( zxoj)&%9^p>Wr&^C(B`^(Qe>;q*&CR^2yVT#|}0Aq@RF2Gk~U!e$Sw)$u$ ze+Y|8wyKla7xBF}H>{Js((;`zCo6g#fTiJRa*B^hr92UhW$o=(c#u`|UjkCG8d={f%%+S== z*f#_EAO?1D0jcPmyrI~Iq`N=rYP3io0*fQs&7pi5N_H8@ah{ei35LN$1-cP}8gs~G z|KFjm;b!V@Q37oYtVeY%JZwHtA~X5y}95;eWF3Y9Y~iQCss`}Pg48d zNk>}5U$(aE2yq0)SMl_#}3`PoD2 z!z(RAkD^M2tt=uz%Xh3u!rai9I)92?gvHKT1i;1+Y^CaqgbQ?D6vIf<^C48!eH_h6 zD=EpxoH6wD%*P^_ql55A)fS)~wa2Y&^Y$>B$?ZMi=zcb`rm-Ra$&~cu!HQ?gZF-&(4`6d+L5@WAlV4hUjEY zv9lG6$R61#v3O&5%_<3n^ejk?klu7EEB1wg99zp1-_a7+Jc%WAI|`JmR(Ol@bhH3H zZ>&hX2^<@Q+sa;&JBtIJG|a_J7~BU z<|&Y1Y3ze+`N!XrfS`=dS`14)75JtJ1~s|-KGIvnDL)aU}is*k|edzmp zV&zknUu@t7gS8fdRolJxo%q{@il8m{qS(F(#jf(7%BE9Zcz|*=#AjZTz`9%`a$9AL zv#-UrfI~0&5@rdhh||Y92kj(JEeyAJjNy16oYEM>KE|6qgV|Qhd((C|1~&@I7JvEh zW4MDh(cv{9i;0p=s{fiz6_mco>3{pCbxPkPE-ofrNhYu7W+r92zYuSxCK(wk^Fdm< z-XkT!`CaXnQO{2eSDa!D*8OcNZ`Ha~(H^eeh_9arcYy2F{7&7m4)2WyZlwe)g{XZtJR5Txkk;UcF}kCgKTziE6;E2R`xWy=M;wT{R07q?&<&l literal 0 HcmV?d00001 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index a455a3b841..b271ea566c 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; +import android.util.SparseArray; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -73,7 +74,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { } public void testCustomPesReader() throws Exception { - CustomEsReaderFactory factory = new CustomEsReaderFactory(); + CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) @@ -82,13 +83,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { .setSimulatePartialReads(false).build(); FakeExtractorOutput output = new FakeExtractorOutput(); tsExtractor.init(output); - tsExtractor.seek(input.getPosition()); PositionHolder seekPositionHolder = new PositionHolder(); int readResult = Extractor.RESULT_CONTINUE; while (readResult != Extractor.RESULT_END_OF_INPUT) { readResult = tsExtractor.read(input, seekPositionHolder); } - CustomEsReader reader = factory.reader; + CustomEsReader reader = factory.esReader; assertEquals(2, reader.packetsRead); TrackOutput trackOutput = reader.getTrackOutput(); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); @@ -97,6 +97,23 @@ public final class TsExtractorTest extends InstrumentationTestCase { ((FakeTrackOutput) trackOutput).format); } + public void testCustomInitialSectionReader() throws Exception { + CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); + TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); + FakeExtractorInput input = new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) + .setSimulateIOErrors(false) + .setSimulateUnknownLength(false) + .setSimulatePartialReads(false).build(); + tsExtractor.init(new FakeExtractorOutput()); + PositionHolder seekPositionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + while (readResult != Extractor.RESULT_END_OF_INPUT) { + readResult = tsExtractor.read(input, seekPositionHolder); + } + assertEquals(1, factory.sdtReader.consumedSdts); + } + private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException { for (int i = 0; i < length; i++) { if (((byte) i) == TS_SYNC_BYTE) { @@ -107,6 +124,45 @@ public final class TsExtractorTest extends InstrumentationTestCase { } } + private static final class CustomTsPayloadReaderFactory implements TsPayloadReader.Factory { + + private final boolean provideSdtReader; + private final boolean provideCustomEsReader; + private final TsPayloadReader.Factory defaultFactory; + private CustomEsReader esReader; + private SdtSectionReader sdtReader; + + public CustomTsPayloadReaderFactory(boolean provideCustomEsReader, boolean provideSdtReader) { + this.provideCustomEsReader = provideCustomEsReader; + this.provideSdtReader = provideSdtReader; + defaultFactory = new DefaultTsPayloadReaderFactory(); + } + + @Override + public SparseArray createInitialPayloadReaders() { + if (provideSdtReader) { + assertNull(sdtReader); + SparseArray mapping = new SparseArray<>(); + sdtReader = new SdtSectionReader(); + mapping.put(17, new SectionReader(sdtReader)); + return mapping; + } else { + return defaultFactory.createInitialPayloadReaders(); + } + } + + @Override + public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { + if (provideCustomEsReader && streamType == 3) { + esReader = new CustomEsReader(esInfo.language); + return new PesReader(esReader); + } else { + return defaultFactory.createPayloadReader(streamType, esInfo); + } + } + + } + private static final class CustomEsReader implements ElementaryStreamReader { private final String language; @@ -147,23 +203,38 @@ public final class TsExtractorTest extends InstrumentationTestCase { } - private static final class CustomEsReaderFactory implements TsPayloadReader.Factory { + private static final class SdtSectionReader implements SectionPayloadReader { - private final TsPayloadReader.Factory defaultFactory; - private CustomEsReader reader; - - public CustomEsReaderFactory() { - defaultFactory = new DefaultTsPayloadReaderFactory(); - } + private int consumedSdts; @Override - public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { - if (streamType == 3) { - reader = new CustomEsReader(esInfo.language); - return new PesReader(reader); - } else { - return defaultFactory.createPayloadReader(streamType, esInfo); + public void consume(ParsableByteArray sectionData) { + // table_id(8), section_syntax_indicator(1), reserved_future_use(1), reserved(2), + // section_length(12), transport_stream_id(16), reserved(2), version_number(5), + // current_next_indicator(1), section_number(8), last_section_number(8), + // original_network_id(16), reserved_future_use(8) + sectionData.skipBytes(11); + // Start of the service loop. + assertEquals(0x5566 /* arbitrary service id */, sectionData.readUnsignedShort()); + // reserved_future_use(6), EIT_schedule_flag(1), EIT_present_following_flag(1) + sectionData.skipBytes(1); + // Assert there is only one service. + // Remove running_status(3), free_CA_mode(1) from the descriptors_loop_length with the mask. + assertEquals(sectionData.readUnsignedShort() & 0xFFF, sectionData.bytesLeft()); + while (sectionData.bytesLeft() > 0) { + int descriptorTag = sectionData.readUnsignedByte(); + int descriptorLength = sectionData.readUnsignedByte(); + if (descriptorTag == 72 /* service descriptor */) { + assertEquals(1, sectionData.readUnsignedByte()); // Service type: Digital TV. + int serviceProviderNameLength = sectionData.readUnsignedByte(); + assertEquals("Some provider", sectionData.readString(serviceProviderNameLength)); + int serviceNameLength = sectionData.readUnsignedByte(); + assertEquals("Some Channel", sectionData.readString(serviceNameLength)); + } else { + sectionData.skipBytes(descriptorLength); + } } + consumedSdts++; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 5aabc29a5d..96f7ff423f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; +import android.util.SparseArray; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -49,6 +50,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact this.flags = flags; } + @Override + public SparseArray createInitialPayloadReaders() { + return new SparseArray<>(); + } + @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index b52b5cc047..6913207529 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -253,6 +253,12 @@ public final class TsExtractor implements Extractor { private void resetPayloadReaders() { trackIds.clear(); tsPayloadReaders.clear(); + SparseArray initialPayloadReaders = + payloadReaderFactory.createInitialPayloadReaders(); + int initialPayloadReadersSize = initialPayloadReaders.size(); + for (int i = 0; i < initialPayloadReadersSize; i++) { + tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i)); + } tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader())); id3Reader = null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 28e9fb9095..3916be39c9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.util.SparseArray; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -30,6 +31,15 @@ public interface TsPayloadReader { */ interface Factory { + /** + * Returns the initial mapping from PIDs to payload readers. + *

+ * This method allows the injection of payload readers for reserved PIDs, excluding PID 0. + * + * @return A {@link SparseArray} that maps PIDs to payload readers. + */ + SparseArray createInitialPayloadReaders(); + /** * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information. * May return null if the stream type is not supported. From 0b8e9754ca7a8b2634aa3ebaa85681b51b540743 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 25 Oct 2016 09:00:19 -0700 Subject: [PATCH 053/206] Pass initialization parameters to section readers Unlike with PesReaders, sections don't have a standard way of providing timestmaps or even generating tracks. We need to pass this information so that readers decide what to do with it. Issue:#726 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137162494 --- .../exoplayer2/extractor/ts/TsExtractorTest.java | 6 ++++++ .../extractor/ts/SectionPayloadReader.java | 15 +++++++++++++++ .../exoplayer2/extractor/ts/SectionReader.java | 3 +-- .../exoplayer2/extractor/ts/TsExtractor.java | 14 ++++++++++++-- .../exoplayer2/extractor/ts/TsPayloadReader.java | 7 ++++--- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index b271ea566c..58893f15c1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -207,6 +207,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { private int consumedSdts; + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @Override public void consume(ParsableByteArray sectionData) { // table_id(8), section_syntax_indicator(1), reserved_future_use(1), reserved(2), diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java index 9be41af594..2bddc56582 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; /** @@ -22,6 +26,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public interface SectionPayloadReader { + /** + * Initializes the section payload reader. + * + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. + */ + void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator); + /** * Called by a {@link SectionReader} when a full section is received. * diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index ccf00f8d19..9a181897ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -43,8 +43,7 @@ public final class SectionReader implements TsPayloadReader { @Override public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - // TODO: Injectable section readers might want to generate metadata tracks. - // Do nothing. + reader.init(timestampAdjuster, extractorOutput, idGenerator); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 6913207529..219101b8d3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -65,8 +65,6 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; - private static final String TAG = "TsExtractor"; - private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_PAT_PID = 0; @@ -274,6 +272,12 @@ public final class TsExtractor implements Extractor { patScratch = new ParsableBitArray(new byte[4]); } + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @Override public void consume(ParsableByteArray sectionData) { // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), @@ -316,6 +320,12 @@ public final class TsExtractor implements Extractor { this.pid = pid; } + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + @Override public void consume(ParsableByteArray sectionData) { // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 3916be39c9..304c8c1282 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -99,9 +99,10 @@ public interface TsPayloadReader { /** * Initializes the payload reader. * - * @param timestampAdjuster - * @param extractorOutput - * @param idGenerator + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. */ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TrackIdGenerator idGenerator); From 2c54363204b04bfd58375f30eb6ccc214ef818c3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 25 Oct 2016 11:46:55 -0700 Subject: [PATCH 054/206] Report track groups and selections through ExoPlayer TrackSelector no longer has a listener. Instead, tracks change events are reported through ExoPlayer.EventListener. Applications interested in retrieving the selection info should retrieve it directly from the TrackSelector by calling an exposed getter. Pretty sure the ref'd issue is fixed as a side effect of this change. Issue: #1942 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137183073 --- .../android/exoplayer2/demo/EventLogger.java | 44 +++++---- .../exoplayer2/demo/PlayerActivity.java | 47 +++++----- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../exoplayer2/DefaultLoadControl.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 43 ++++++++- .../android/exoplayer2/ExoPlayerFactory.java | 12 +-- .../android/exoplayer2/ExoPlayerImpl.java | 79 +++++++++++++--- .../exoplayer2/ExoPlayerImplInternal.java | 94 ++++++++++++------- .../android/exoplayer2/LoadControl.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 72 +++++--------- .../exoplayer2/source/TrackGroupArray.java | 5 + .../trackselection/DefaultTrackSelector.java | 14 +-- .../trackselection/MappingTrackSelector.java | 42 ++++++--- ...lections.java => TrackSelectionArray.java} | 12 +-- .../trackselection/TrackSelector.java | 92 +++--------------- .../exoplayer2/ui/DebugTextViewHelper.java | 7 ++ .../exoplayer2/ui/PlaybackControlView.java | 7 ++ .../exoplayer2/ui/SimpleExoPlayerView.java | 11 ++- .../playbacktests/gts/DashTest.java | 3 - .../playbacktests/util/ExoHostedTest.java | 9 +- 22 files changed, 355 insertions(+), 276 deletions(-) rename library/src/main/java/com/google/android/exoplayer2/trackselection/{TrackSelections.java => TrackSelectionArray.java} (85%) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index d79de04657..f6554e71a8 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,10 +38,10 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelections; -import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; @@ -55,7 +55,7 @@ import java.util.Locale; /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, - TrackSelector.EventListener, MetadataRenderer.Output> { + MetadataRenderer.Output> { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -67,11 +67,13 @@ import java.util.Locale; TIME_FORMAT.setGroupingUsed(false); } + private final MappingTrackSelector trackSelector; private final Timeline.Window window; private final Timeline.Period period; private final long startTimeMs; - public EventLogger() { + public EventLogger(MappingTrackSelector trackSelector) { + this.trackSelector = trackSelector; window = new Timeline.Window(); period = new Timeline.Period(); startTimeMs = SystemClock.elapsedRealtime(); @@ -126,27 +128,29 @@ import java.util.Locale; Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); } - // MappingTrackSelector.EventListener - @Override - public void onTrackSelectionsChanged(TrackSelections trackSelections) { + public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo == null) { + Log.d(TAG, "Tracks []"); + return; + } Log.d(TAG, "Tracks ["); // Log tracks associated to renderers. - MappedTrackInfo info = trackSelections.info; - for (int rendererIndex = 0; rendererIndex < trackSelections.length; rendererIndex++) { - TrackGroupArray trackGroups = info.getTrackGroups(rendererIndex); + for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); TrackSelection trackSelection = trackSelections.get(rendererIndex); - if (trackGroups.length > 0) { + if (rendererTrackGroups.length > 0) { Log.d(TAG, " Renderer:" + rendererIndex + " ["); - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup trackGroup = trackGroups.get(groupIndex); - String adaptiveSupport = getAdaptiveSupportString( - trackGroup.length, info.getAdaptiveSupport(rendererIndex, groupIndex, false)); + for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { + TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); + String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, + mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = getFormatSupportString( - info.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); + mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); Log.d(TAG, " " + status + " Track:" + trackIndex + ", " + getFormatString(trackGroup.getFormat(trackIndex)) + ", supported=" + formatSupport); @@ -157,12 +161,12 @@ import java.util.Locale; } } // Log tracks not associated with a renderer. - TrackGroupArray trackGroups = info.getUnassociatedTrackGroups(); - if (trackGroups.length > 0) { + TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); + if (unassociatedTrackGroups.length > 0) { Log.d(TAG, " Renderer:None ["); - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { + for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { Log.d(TAG, " Group:" + groupIndex + " ["); - TrackGroup trackGroup = trackGroups.get(groupIndex); + TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = getFormatSupportString( diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index e9aa46f85f..5351890d6f 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -58,8 +58,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelections; -import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; @@ -78,7 +77,7 @@ import java.util.UUID; * An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener, - TrackSelector.EventListener, PlaybackControlView.VisibilityListener { + PlaybackControlView.VisibilityListener { public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; public static final String DRM_LICENSE_URL = "drm_license_url"; @@ -203,8 +202,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (view == retryButton) { initializePlayer(); } else if (view.getParent() == debugRootView) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentSelections().info, (int) view.getTag()); + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), + trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + } } } @@ -249,20 +251,20 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } - eventLogger = new EventLogger(); TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory); - trackSelector.addListener(this); - trackSelector.addListener(eventLogger); + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), drmSessionManager, preferExtensionDecoders); player.addListener(this); + + eventLogger = new EventLogger(trackSelector); player.addListener(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); player.setId3Output(eventLogger); + simpleExoPlayerView.setPlayer(player); if (isTimelineStatic) { if (playerPosition == C.TIME_UNSET) { @@ -447,17 +449,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay showControls(); } - // MappingTrackSelector.EventListener implementation - @Override - public void onTrackSelectionsChanged(TrackSelections trackSelections) { + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { updateButtonVisibilities(); - MappedTrackInfo trackInfo = trackSelections.info; - if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_VIDEO)) { - showToast(R.string.error_unsupported_video); - } - if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_AUDIO)) { - showToast(R.string.error_unsupported_audio); + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_VIDEO)) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_AUDIO)) { + showToast(R.string.error_unsupported_audio); + } } } @@ -473,14 +475,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay return; } - TrackSelections trackSelections = trackSelector.getCurrentSelections(); - if (trackSelections == null) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo == null) { return; } - int rendererCount = trackSelections.length; - for (int i = 0; i < rendererCount; i++) { - TrackGroupArray trackGroups = trackSelections.info.getTrackGroups(i); + for (int i = 0; i < mappedTrackInfo.length; i++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); if (trackGroups.length != 0) { Button button = new Button(this); int label; diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 29a22f380a..990c470a93 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac; import android.content.Context; import android.net.Uri; -import android.os.Handler; import android.os.Looper; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ExoPlaybackException; @@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -72,7 +73,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { public void run() { Looper.prepare(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); ExtractorMediaSource mediaSource = new ExtractorMediaSource( @@ -91,6 +92,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { // Do nothing. diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 4f723698a4..3e07186995 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.opus; import android.content.Context; import android.net.Uri; -import android.os.Handler; import android.os.Looper; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ExoPlaybackException; @@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -72,7 +73,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { public void run() { Looper.prepare(); LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); ExtractorMediaSource mediaSource = new ExtractorMediaSource( @@ -91,6 +92,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { // Do nothing. diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index c5f61cf231..b1ddf2368c 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9; import android.content.Context; import android.net.Uri; -import android.os.Handler; import android.os.Looper; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ExoPlaybackException; @@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -88,7 +89,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { public void run() { Looper.prepare(); LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); ExtractorMediaSource mediaSource = new ExtractorMediaSource( @@ -110,6 +111,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { // Do nothing. diff --git a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index e7a0f8b1b8..e6a39d8a27 100644 --- a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.Util; @@ -111,7 +111,7 @@ public final class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, - TrackSelections trackSelections) { + TrackSelectionArray trackSelections) { targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { if (trackSelections.get(i) != null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index e3c9b6e114..31efdd82b1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -22,11 +22,13 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; @@ -110,6 +112,15 @@ public interface ExoPlayer { */ interface EventListener { + /** + * Called when the available or selected tracks change. + * + * @param trackGroups The available tracks. Never null, but may be of length zero. + * @param trackSelections The track selections for each {@link Renderer}. Never null and always + * of length {@link #getRendererCount()}, but may contain null elements. + */ + void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); + /** * Called when the player starts or stops loading the source. * @@ -259,11 +270,11 @@ public interface ExoPlayer { * @param resetPosition Whether the playback position should be reset to the default position in * the first {@link Timeline.Window}. If false, playback will start from the position defined * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. - * @param resetTimeline Whether the timeline and manifest should be reset. Should be true unless - * the player is being prepared to play the same media as it was playing previously (e.g. if - * playback failed and is being retried). + * @param resetState Whether the timeline, manifest, tracks and track selections should be reset. + * Should be true unless the player is being prepared to play the same media as it was playing + * previously (e.g. if playback failed and is being retried). */ - void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline); + void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. @@ -356,6 +367,30 @@ public interface ExoPlayer { */ void blockingSendMessages(ExoPlayerMessage... messages); + /** + * Returns the number of renderers. + */ + int getRendererCount(); + + /** + * Returns the track type that the renderer at a given index handles. + * + * @see Renderer#getTrackType() + * @param index The index of the renderer. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + */ + int getRendererType(int index); + + /** + * Returns the available track groups. + */ + TrackGroupArray getCurrentTrackGroups(); + + /** + * Returns the current track selections for each renderer. + */ + TrackSelectionArray getCurrentTrackSelections(); + /** * Returns the current manifest. The type depends on the {@link MediaSource} passed to * {@link #prepare}. diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 91ab56805a..e43a9c0357 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -42,7 +42,7 @@ public final class ExoPlayerFactory { * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. */ - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl) { return newSimpleInstance(context, trackSelector, loadControl, null); } @@ -57,7 +57,7 @@ public final class ExoPlayerFactory { * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. */ - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager) { return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false); } @@ -75,7 +75,7 @@ public final class ExoPlayerFactory { * available extensions over those defined in the core library. Note that extensions must be * included in the application build for setting this flag to have any effect. */ - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders) { return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, @@ -97,7 +97,7 @@ public final class ExoPlayerFactory { * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * seamlessly join an ongoing playback. */ - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, @@ -111,7 +111,7 @@ public final class ExoPlayerFactory { * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. */ - public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) { + public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) { return newInstance(renderers, trackSelector, new DefaultLoadControl()); } @@ -123,7 +123,7 @@ public final class ExoPlayerFactory { * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. */ - public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, + public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return new ExoPlayerImpl(renderers, trackSelector, loadControl); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 3eb2ceb38b..2e0502c019 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,7 +22,11 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; +import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Assertions; import java.util.concurrent.CopyOnWriteArraySet; @@ -34,12 +38,16 @@ import java.util.concurrent.CopyOnWriteArraySet; private static final String TAG = "ExoPlayerImpl"; + private final Renderer[] renderers; + private final TrackSelector trackSelector; + private final TrackSelectionArray emptyTrackSelections; private final Handler eventHandler; - private final ExoPlayerImplInternal internalPlayer; + private final ExoPlayerImplInternal internalPlayer; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; + private boolean tracksSelected; private boolean pendingInitialSeek; private boolean playWhenReady; private int playbackState; @@ -47,6 +55,8 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean isLoading; private Timeline timeline; private Object manifest; + private TrackGroupArray trackGroups; + private TrackSelectionArray trackSelections; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -63,16 +73,19 @@ import java.util.concurrent.CopyOnWriteArraySet; * @param loadControl The {@link LoadControl} that will be used by the instance. */ @SuppressLint("HandlerLeak") - public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); - Assertions.checkNotNull(renderers); Assertions.checkState(renderers.length > 0); + this.renderers = Assertions.checkNotNull(renderers); + this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; this.playbackState = STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); + emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); window = new Timeline.Window(); period = new Timeline.Period(); + trackGroups = TrackGroupArray.EMPTY; + trackSelections = emptyTrackSelections; eventHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -80,8 +93,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } }; playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); - internalPlayer = new ExoPlayerImplInternal<>(renderers, trackSelector, loadControl, - playWhenReady, eventHandler, playbackInfo); + internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, + eventHandler, playbackInfo); } @Override @@ -105,12 +118,23 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) { - if (resetTimeline && (timeline != null || manifest != null)) { - timeline = null; - manifest = null; - for (EventListener listener : listeners) { - listener.onTimelineChanged(null, null); + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (resetState) { + if (timeline != null || manifest != null) { + timeline = null; + manifest = null; + for (EventListener listener : listeners) { + listener.onTimelineChanged(null, null); + } + } + if (tracksSelected) { + tracksSelected = false; + trackGroups = TrackGroupArray.EMPTY; + trackSelections = emptyTrackSelections; + trackSelector.onSelectionActivated(null); + for (EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } } } internalPlayer.prepare(mediaSource, resetPosition); @@ -266,6 +290,26 @@ import java.util.concurrent.CopyOnWriteArraySet; : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); } + @Override + public int getRendererCount() { + return renderers.length; + } + + @Override + public int getRendererType(int index) { + return renderers[index].getTrackType(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return trackGroups; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return trackSelections; + } + @Override public Timeline getCurrentTimeline() { return timeline; @@ -293,6 +337,17 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; } + case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { + TrackInfo trackInfo = (TrackInfo) msg.obj; + tracksSelected = true; + trackGroups = trackInfo.groups; + trackSelections = trackInfo.selections; + trackSelector.onSelectionActivated(trackInfo.info); + for (EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } + break; + } case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 9d6c435635..6d8f3af0c3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -26,8 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; @@ -40,7 +41,7 @@ import java.io.IOException; /** * Implements the internal behavior of {@link ExoPlayerImpl}. */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, +/* package */ final class ExoPlayerImplInternal implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener { /** @@ -64,15 +65,30 @@ import java.io.IOException; } + public static final class TrackInfo { + + public final TrackGroupArray groups; + public final TrackSelectionArray selections; + public final Object info; + + public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) { + this.groups = groups; + this.selections = selections; + this.info = info; + } + + } + private static final String TAG = "ExoPlayerImplInternal"; // External messages public static final int MSG_STATE_CHANGED = 1; public static final int MSG_LOADING_CHANGED = 2; - public static final int MSG_SEEK_ACK = 3; - public static final int MSG_POSITION_DISCONTINUITY = 4; - public static final int MSG_SOURCE_INFO_REFRESHED = 5; - public static final int MSG_ERROR = 6; + public static final int MSG_TRACKS_CHANGED = 3; + public static final int MSG_SEEK_ACK = 4; + public static final int MSG_POSITION_DISCONTINUITY = 5; + public static final int MSG_SOURCE_INFO_REFRESHED = 6; + public static final int MSG_ERROR = 7; // Internal messages private static final int MSG_PREPARE = 0; @@ -100,7 +116,7 @@ import java.io.IOException; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; - private final TrackSelector trackSelector; + private final TrackSelector trackSelector; private final LoadControl loadControl; private final StandaloneMediaClock standaloneMediaClock; private final Handler handler; @@ -128,13 +144,13 @@ import java.io.IOException; private boolean isTimelineReady; private boolean isTimelineEnded; private int bufferAheadPeriodCount; - private MediaPeriodHolder playingPeriodHolder; - private MediaPeriodHolder readingPeriodHolder; - private MediaPeriodHolder loadingPeriodHolder; + private MediaPeriodHolder playingPeriodHolder; + private MediaPeriodHolder readingPeriodHolder; + private MediaPeriodHolder loadingPeriodHolder; private Timeline timeline; - public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, + public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, boolean playWhenReady, Handler eventHandler, PlaybackInfo playbackInfo) { this.renderers = renderers; @@ -538,7 +554,7 @@ import java.io.IOException; periodIndex = C.INDEX_UNSET; } - MediaPeriodHolder newPlayingPeriodHolder = null; + MediaPeriodHolder newPlayingPeriodHolder = null; if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. if (loadingPeriodHolder != null) { @@ -546,7 +562,7 @@ import java.io.IOException; } } else { // Clear the timeline, but keep the requested period if it is already prepared. - MediaPeriodHolder periodHolder = playingPeriodHolder; + MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { if (periodHolder.index == periodIndex && periodHolder.prepared) { newPlayingPeriodHolder = periodHolder; @@ -680,7 +696,7 @@ import java.io.IOException; return; } // Reselect tracks on each period in turn, until the selection changes. - MediaPeriodHolder periodHolder = playingPeriodHolder; + MediaPeriodHolder periodHolder = playingPeriodHolder; boolean selectionsChangedForReadPeriod = true; while (true) { if (periodHolder == null || !periodHolder.prepared) { @@ -745,7 +761,7 @@ import java.io.IOException; } } } - trackSelector.onSelectionActivated(playingPeriodHolder.trackSelections); + eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -817,11 +833,11 @@ import java.io.IOException; playingPeriodHolder.setIndex(timeline, timeline.getWindow(period.windowIndex, window), index); - MediaPeriodHolder previousPeriodHolder = playingPeriodHolder; + MediaPeriodHolder previousPeriodHolder = playingPeriodHolder; boolean seenReadingPeriod = false; bufferAheadPeriodCount = 0; while (previousPeriodHolder.next != null) { - MediaPeriodHolder periodHolder = previousPeriodHolder.next; + MediaPeriodHolder periodHolder = previousPeriodHolder.next; index++; timeline.getPeriod(index, period, true); if (!periodHolder.uid.equals(period.uid)) { @@ -962,9 +978,8 @@ import java.io.IOException; MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, loadControl.getAllocator(), periodStartPositionUs); newMediaPeriod.prepare(this); - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder<>(renderers, - rendererCapabilities, trackSelector, mediaSource, newMediaPeriod, newPeriodUid, - periodStartPositionUs); + MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, + trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); timeline.getWindow(windowIndex, window); newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); if (loadingPeriodHolder != null) { @@ -1018,9 +1033,9 @@ import java.io.IOException; } } if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { - TrackSelections oldTrackSelections = readingPeriodHolder.trackSelections; + TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections; readingPeriodHolder = readingPeriodHolder.next; - TrackSelections newTrackSelections = readingPeriodHolder.trackSelections; + TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; TrackSelection oldSelection = oldTrackSelections.get(i); @@ -1094,14 +1109,14 @@ import java.io.IOException; } } - private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { + private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { while (periodHolder != null) { periodHolder.release(); periodHolder = periodHolder.next; } } - private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) + private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; @@ -1125,7 +1140,7 @@ import java.io.IOException; } } - trackSelector.onSelectionActivated(periodHolder.trackSelections); + eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); playingPeriodHolder = periodHolder; enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1182,7 +1197,7 @@ import java.io.IOException; /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ - private static final class MediaPeriodHolder { + private static final class MediaPeriodHolder { public final MediaPeriod mediaPeriod; public final Object uid; @@ -1196,19 +1211,21 @@ import java.io.IOException; public boolean prepared; public boolean hasEnabledTracks; public long rendererPositionOffsetUs; - public MediaPeriodHolder next; + public MediaPeriodHolder next; public boolean needsContinueLoading; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; - private final TrackSelector trackSelector; + private final TrackSelector trackSelector; private final MediaSource mediaSource; - private TrackSelections trackSelections; - private TrackSelections periodTrackSelections; + private Object trackSelectionsInfo; + private TrackGroupArray trackGroups; + private TrackSelectionArray trackSelections; + private TrackSelectionArray periodTrackSelections; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - TrackSelector trackSelector, MediaSource mediaSource, MediaPeriod mediaPeriod, + TrackSelector trackSelector, MediaSource mediaSource, MediaPeriod mediaPeriod, Object uid, long positionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; @@ -1221,7 +1238,7 @@ import java.io.IOException; startPositionUs = positionUs; } - public void setNext(MediaPeriodHolder next) { + public void setNext(MediaPeriodHolder next) { this.next = next; } @@ -1238,17 +1255,20 @@ import java.io.IOException; public void handlePrepared(long positionUs, LoadControl loadControl) throws ExoPlaybackException { prepared = true; + trackGroups = mediaPeriod.getTrackGroups(); selectTracks(); startPositionUs = updatePeriodTrackSelection(positionUs, loadControl, false); } public boolean selectTracks() throws ExoPlaybackException { - TrackSelections newTrackSelections = trackSelector.selectTracks(rendererCapabilities, - mediaPeriod.getTrackGroups()); + Pair selectorResult = trackSelector.selectTracks( + rendererCapabilities, trackGroups); + TrackSelectionArray newTrackSelections = selectorResult.first; if (newTrackSelections.equals(periodTrackSelections)) { return false; } trackSelections = newTrackSelections; + trackSelectionsInfo = selectorResult.second; return true; } @@ -1283,10 +1303,14 @@ import java.io.IOException; } // The track selection has changed. - loadControl.onTracksSelected(renderers, mediaPeriod.getTrackGroups(), trackSelections); + loadControl.onTracksSelected(renderers, trackGroups, trackSelections); return positionUs; } + public TrackInfo getTrackInfo() { + return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo); + } + public void release() { try { mediaSource.releasePeriod(mediaPeriod); diff --git a/library/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/src/main/java/com/google/android/exoplayer2/LoadControl.java index 6176c6085b..c092480222 100644 --- a/library/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; /** @@ -38,7 +38,7 @@ public interface LoadControl { * @param trackSelections The track selections that were made. */ void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, - TrackSelections trackSelections); + TrackSelectionArray trackSelections); /** * Called by the player when stopped. diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 4b673d3750..d9c405cad6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -39,9 +39,10 @@ import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; -import com.google.android.exoplayer2.trackselection.TrackSelections; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -86,11 +87,6 @@ public final class SimpleExoPlayer implements ExoPlayer { */ void onRenderedFirstFrame(); - /** - * Called when a video track is no longer selected. - */ - void onVideoTracksDisabled(); - } private static final String TAG = "SimpleExoPlayer"; @@ -103,7 +99,6 @@ public final class SimpleExoPlayer implements ExoPlayer { private final int videoRendererCount; private final int audioRendererCount; - private boolean videoTracksEnabled; private Format videoFormat; private Format audioFormat; @@ -122,12 +117,11 @@ public final class SimpleExoPlayer implements ExoPlayer { private float volume; private PlaybackParamsHolder playbackParamsHolder; - /* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector, + /* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { mainHandler = new Handler(); componentListener = new ComponentListener(); - trackSelector.addListener(componentListener); // Build the renderers. ArrayList renderersList = new ArrayList<>(); @@ -164,26 +158,6 @@ public final class SimpleExoPlayer implements ExoPlayer { player = new ExoPlayerImpl(renderers, trackSelector, loadControl); } - /** - * Returns the number of renderers. - * - * @return The number of renderers. - */ - public int getRendererCount() { - return renderers.length; - } - - /** - * Returns the track type that the renderer at a given index handles. - * - * @see Renderer#getTrackType() - * @param index The index of the renderer. - * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. - */ - public int getRendererType(int index) { - return renderers[index].getTrackType(); - } - /** * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} * currently set on the player. @@ -517,6 +491,26 @@ public final class SimpleExoPlayer implements ExoPlayer { return player.getBufferedPercentage(); } + @Override + public int getRendererCount() { + return player.getRendererCount(); + } + + @Override + public int getRendererType(int index) { + return player.getRendererType(index); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return player.getCurrentTrackGroups(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return player.getCurrentTrackSelections(); + } + @Override public Timeline getCurrentTimeline() { return player.getCurrentTimeline(); @@ -651,8 +645,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output>, - SurfaceHolder.Callback, TextureView.SurfaceTextureListener, - TrackSelector.EventListener { + SurfaceHolder.Callback, TextureView.SurfaceTextureListener { // VideoRendererEventListener implementation @@ -831,23 +824,6 @@ public final class SimpleExoPlayer implements ExoPlayer { // Do nothing. } - // TrackSelector.EventListener implementation - - @Override - public void onTrackSelectionsChanged(TrackSelections trackSelections) { - boolean videoTracksEnabled = false; - for (int i = 0; i < renderers.length; i++) { - if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelections.get(i) != null) { - videoTracksEnabled = true; - break; - } - } - if (videoListener != null && SimpleExoPlayer.this.videoTracksEnabled && !videoTracksEnabled) { - videoListener.onVideoTracksDisabled(); - } - SimpleExoPlayer.this.videoTracksEnabled = videoTracksEnabled; - } - } @TargetApi(23) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index d562ec43e1..394cec891b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -23,6 +23,11 @@ import java.util.Arrays; */ public final class TrackGroupArray { + /** + * The empty array. + */ + public static final TrackGroupArray EMPTY = new TrackGroupArray(); + /** * The number of groups in the array. Greater than or equal to zero. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 81d79ac055..02c2defdfc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.trackselection; import android.content.Context; import android.graphics.Point; -import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -326,25 +325,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Constructs an instance that does not support adaptive video. - * - * @param eventHandler A handler to use when delivering events to listeners. May be null if - * listeners will not be added. */ - public DefaultTrackSelector(Handler eventHandler) { - this(eventHandler, null); + public DefaultTrackSelector() { + this(null); } /** * Constructs an instance that uses a factory to create adaptive video track selections. * - * @param eventHandler A handler to use when delivering events to listeners. May be null if - * listeners will not be added. * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, * or null if the selector should not support adaptive video. */ - public DefaultTrackSelector(Handler eventHandler, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { - super(eventHandler); + public DefaultTrackSelector(TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; params = new AtomicReference<>(new Parameters()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 3307fc3baa..7454ff6801 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -15,14 +15,13 @@ */ package com.google.android.exoplayer2.trackselection; -import android.os.Handler; +import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.HashMap; @@ -32,7 +31,7 @@ import java.util.Map; * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer. */ -public abstract class MappingTrackSelector extends TrackSelector { +public abstract class MappingTrackSelector extends TrackSelector { /** * A track selection override. @@ -83,16 +82,21 @@ public abstract class MappingTrackSelector extends TrackSelector> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - /** - * @param eventHandler A handler to use when delivering events to listeners added via - * {@link #addListener(EventListener)}. - */ - public MappingTrackSelector(Handler eventHandler) { - super(eventHandler); + private MappedTrackInfo currentMappedTrackInfo; + + public MappingTrackSelector() { selectionOverrides = new SparseArray<>(); rendererDisabledFlags = new SparseBooleanArray(); } + /** + * Returns the mapping information associated with the current track selections, or null if no + * selection is currently active. + */ + public final MappedTrackInfo getCurrentMappedTrackInfo() { + return currentMappedTrackInfo; + } + /** * Sets whether the renderer at the specified index is disabled. * @@ -224,7 +228,7 @@ public abstract class MappingTrackSelector extends TrackSelector selectTracks( + public final Pair selectTracks( RendererCapabilities[] rendererCapabilities, TrackGroupArray trackGroups) throws ExoPlaybackException { // Structures into which data will be written during the selection. The extra item at the end @@ -294,7 +298,13 @@ public abstract class MappingTrackSelector extends TrackSelector(mappedTrackInfo, trackSelections); + return Pair.create(new TrackSelectionArray(trackSelections), + mappedTrackInfo); + } + + @Override + public final void onSelectionActivated(Object info) { + currentMappedTrackInfo = (MappedTrackInfo) info; } /** @@ -409,12 +419,16 @@ public abstract class MappingTrackSelector extends TrackSelector { +public final class TrackSelectionArray { - /** - * Opaque information associated with the result. - */ - public final T info; /** * The number of selections in the result. Greater than or equal to zero. */ @@ -37,11 +33,9 @@ public final class TrackSelections { private int hashCode; /** - * @param info Opaque information associated with the result. * @param trackSelections The selections. Must not be null, but may contain null elements. */ - public TrackSelections(T info, TrackSelection... trackSelections) { - this.info = info; + public TrackSelectionArray(TrackSelection... trackSelections) { this.trackSelections = trackSelections; this.length = trackSelections.length; } @@ -81,7 +75,7 @@ public final class TrackSelections { if (obj == null || getClass() != obj.getClass()) { return false; } - TrackSelections other = (TrackSelections) obj; + TrackSelectionArray other = (TrackSelectionArray) obj; return Arrays.equals(trackSelections, other.trackSelections); } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index 9c859312cb..5a9d3923bf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -15,15 +15,13 @@ */ package com.google.android.exoplayer2.trackselection; -import android.os.Handler; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.util.Assertions; -import java.util.concurrent.CopyOnWriteArraySet; /** Selects tracks to be consumed by available renderers. */ -public abstract class TrackSelector { +public abstract class TrackSelector { /** * Notified when previous selections by a {@link TrackSelector} are no longer valid. @@ -37,56 +35,7 @@ public abstract class TrackSelector { } - /** Listener of {@link TrackSelector} events. */ - public interface EventListener { - - /** - * Called when the track selections have changed. - * - * @param trackSelections The new track selections. - */ - void onTrackSelectionsChanged(TrackSelections trackSelections); - - } - - private final Handler eventHandler; - private final CopyOnWriteArraySet> listeners; - private InvalidationListener listener; - private TrackSelections activeSelections; - - /** - * @param eventHandler A handler to use when delivering events to listeners added via {@link - * #addListener(EventListener)}. - */ - public TrackSelector(Handler eventHandler) { - this.eventHandler = Assertions.checkNotNull(eventHandler); - this.listeners = new CopyOnWriteArraySet<>(); - } - - /** - * Registers a listener to receive events from the selector. The listener's methods will be called - * using the {@link Handler} that was passed to the constructor. - * - * @param listener The listener to register. - */ - public final void addListener(EventListener listener) { - listeners.add(listener); - } - - /** - * Unregister a listener. The listener will no longer receive events from the selector. - * - * @param listener The listener to unregister. - */ - public final void removeListener(EventListener listener) { - listeners.remove(listener); - } - - /** Returns the current track selections. */ - public final TrackSelections getCurrentSelections() { - return activeSelections; - } /** * Initializes the selector. @@ -98,28 +47,27 @@ public abstract class TrackSelector { } /** - * Generates {@link TrackSelections} for the renderers. + * Generates {@link TrackSelectionArray} for the renderers. * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which {@link - * TrackSelection}s are to be generated. + * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which + * {@link TrackSelection}s are to be generated. * @param trackGroups The available track groups. - * @return The track selections. + * @return The track selections, and an implementation specific object that will be returned to + * the selector via {@link #onSelectionActivated(Object)} should the selections be activated. * @throws ExoPlaybackException If an error occurs selecting tracks. */ - public abstract TrackSelections selectTracks( + public abstract Pair selectTracks( RendererCapabilities[] rendererCapabilities, TrackGroupArray trackGroups) throws ExoPlaybackException; /** - * Called when {@link TrackSelections} previously generated by {@link - * #selectTracks(RendererCapabilities[], TrackGroupArray)} are activated. + * Called when {@link TrackSelectionArray} previously generated by + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} are activated. * - * @param activeSelections The activated {@link TrackSelections}. + * @param info The information associated with the selections, or null if the selected tracks are + * being cleared. */ - public final void onSelectionActivated(TrackSelections activeSelections) { - this.activeSelections = activeSelections; - notifyTrackSelectionsChanged(activeSelections); - } + public abstract void onSelectionActivated(Object info); /** * Invalidates all previously generated track selections. @@ -130,18 +78,4 @@ public abstract class TrackSelector { } } - private void notifyTrackSelectionsChanged(final TrackSelections activeSelections) { - if (eventHandler != null) { - eventHandler.post( - new Runnable() { - @Override - public void run() { - for (EventListener listener : listeners) { - listener.onTrackSelectionsChanged(activeSelections); - } - } - }); - } - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index af38836fc9..1bf5b59a4a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -22,6 +22,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from @@ -98,6 +100,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe // Do nothing. } + @Override + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + // Do nothing. + } + // Runnable implementation. @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 470e173c02..49e1b6bafb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.R; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; import java.util.Formatter; import java.util.Locale; @@ -576,6 +578,11 @@ public class PlaybackControlView extends FrameLayout { // Do nothing. } + @Override + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { // Do nothing. diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 692ff70ce1..198e6870e8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -27,13 +27,16 @@ import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.R; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.List; /** @@ -325,7 +328,13 @@ public final class SimpleExoPlayerView extends FrameLayout { } @Override - public void onVideoTracksDisabled() { + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + for (int i = 0; i < selections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + return; + } + } + // No enabled video renderers. Close the shutter. shutterView.setVisibility(VISIBLE); } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 9e9a03a277..8aee627993 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -19,8 +19,6 @@ import android.annotation.TargetApi; import android.media.MediaDrm; import android.media.UnsupportedSchemeException; import android.net.Uri; -import android.os.Handler; -import android.os.Looper; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import com.google.android.exoplayer2.C; @@ -805,7 +803,6 @@ public final class DashTest extends ActivityInstrumentationTestCase2 Date: Thu, 27 Oct 2016 12:47:26 +0100 Subject: [PATCH 055/206] Add artwork support to SimpleExoPlayerView + misc improvements --- .../extractor/mp4/MetadataUtil.java | 52 ++-- .../exoplayer2/ui/AspectRatioFrameLayout.java | 15 +- .../exoplayer2/ui/PlaybackControlView.java | 8 +- .../exoplayer2/ui/SimpleExoPlayerView.java | 239 ++++++++++++++---- .../res/layout/exo_playback_control_view.xml | 18 +- .../res/layout/exo_simple_player_view.xml | 13 +- library/src/main/res/values/attrs.xml | 13 +- library/src/main/res/values/ids.xml | 24 +- 8 files changed, 271 insertions(+), 111 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index 4bfef85d10..e99dab053b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -71,36 +71,30 @@ import com.google.android.exoplayer2.util.Util; // Standard genres. private static final String[] STANDARD_GENRES = new String[] { // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", - "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", - "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", - "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", - "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", - "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", - "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", - "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", - "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", + "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", + "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", + "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", + "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", + "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", + "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", + "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", + "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", + "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", - // These were made up by the authors of Winamp but backported into the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", - "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", - "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", - "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", - "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", - "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", - "Dance Hall", - // These were also invented by the Winamp folks but ignored by the ID3 authors. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", - "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", - "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", - "Synthpop" + // These were made up by the authors of Winamp and later added to the ID3 spec. + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", + "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", + "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", + "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", + "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", + "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", + "Euro-House", "Dance Hall", + // These were med up by the authors of Winamp but have not been added to the ID3 spec. + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", + "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", + "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", + "Jpop", "Synthpop" }; private static final String LANGUAGE_UNDEFINED = "und"; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index e6f18c882b..f92903d65a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -17,16 +17,26 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; +import android.support.annotation.IntDef; import android.util.AttributeSet; import android.widget.FrameLayout; import com.google.android.exoplayer2.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * A {@link FrameLayout} that resizes itself to match a specified aspect ratio. */ public final class AspectRatioFrameLayout extends FrameLayout { + /** + * Resize modes for {@link AspectRatioFrameLayout}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT}) + public @interface ResizeMode {} + /** * Either the width or height is decreased to obtain the desired aspect ratio. */ @@ -85,12 +95,11 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Sets the resize mode which can be of value {@link #RESIZE_MODE_FIT}, - * {@link #RESIZE_MODE_FIXED_HEIGHT} or {@link #RESIZE_MODE_FIXED_WIDTH}. + * Sets the resize mode. * * @param resizeMode The resize mode. */ - public void setResizeMode(int resizeMode) { + public void setResizeMode(@ResizeMode int resizeMode) { if (this.resizeMode != resizeMode) { this.resizeMode = resizeMode; requestLayout(); diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 49e1b6bafb..03ef4e2f33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -120,7 +120,7 @@ public class PlaybackControlView extends FrameLayout { public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - int layoutResourceId = R.layout.exo_playback_control_view; + int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; @@ -132,8 +132,8 @@ public class PlaybackControlView extends FrameLayout { fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - layoutResourceId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, - layoutResourceId); + controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, + controllerLayoutId); } finally { a.recycle(); } @@ -143,7 +143,7 @@ public class PlaybackControlView extends FrameLayout { formatter = new Formatter(formatBuilder, Locale.getDefault()); componentListener = new ComponentListener(); - LayoutInflater.from(context).inflate(layoutResourceId, this); + LayoutInflater.from(context).inflate(controllerLayoutId, this); time = (TextView) findViewById(R.id.exo_time); timeCurrent = (TextView) findViewById(R.id.exo_time_current); progressBar = (SeekBar) findViewById(R.id.exo_progress); diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 198e6870e8..ecb9319c1d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ui; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.BitmapFactory; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -27,16 +28,22 @@ import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.ImageView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.R; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; +import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -45,15 +52,22 @@ import java.util.List; @TargetApi(16) public final class SimpleExoPlayerView extends FrameLayout { - private final View surfaceView; + private static final int SURFACE_TYPE_NONE = 0; + private static final int SURFACE_TYPE_SURFACE_VIEW = 1; + private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; + + private final ViewGroup videoFrame; + private final AspectRatioFrameLayout aspectRatioVideoFrame; private final View shutterView; - private final SubtitleView subtitleLayout; - private final AspectRatioFrameLayout layout; + private final View surfaceView; + private final ImageView artworkView; + private final SubtitleView subtitleView; private final PlaybackControlView controller; private final ComponentListener componentListener; private SimpleExoPlayer player; - private boolean useController = true; + private boolean useController; + private boolean useArtwork; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -67,7 +81,10 @@ public final class SimpleExoPlayerView extends FrameLayout { public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - boolean useTextureView = false; + int playerLayoutId = R.layout.exo_simple_player_view; + boolean useArtwork = true; + boolean useController = true; + int surfaceType = SURFACE_TYPE_SURFACE_VIEW; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int rewindMs = PlaybackControlView.DEFAULT_REWIND_MS; int fastForwardMs = PlaybackControlView.DEFAULT_FAST_FORWARD_MS; @@ -76,11 +93,12 @@ public final class SimpleExoPlayerView extends FrameLayout { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { + playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, + playerLayoutId); + useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); - useTextureView = a.getBoolean(R.styleable.SimpleExoPlayerView_use_texture_view, - useTextureView); - resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, - AspectRatioFrameLayout.RESIZE_MODE_FIT); + surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); + resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); rewindMs = a.getInt(R.styleable.SimpleExoPlayerView_rewind_increment, rewindMs); fastForwardMs = a.getInt(R.styleable.SimpleExoPlayerView_fastforward_increment, fastForwardMs); @@ -91,35 +109,64 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - LayoutInflater.from(context).inflate(R.layout.exo_simple_player_view, this); + LayoutInflater.from(context).inflate(playerLayoutId, this); componentListener = new ComponentListener(); - layout = (AspectRatioFrameLayout) findViewById(R.id.exo_video_frame); - layout.setResizeMode(resizeMode); - shutterView = findViewById(R.id.exo_shutter); - subtitleLayout = (SubtitleView) findViewById(R.id.exo_subtitles); - subtitleLayout.setUserDefaultStyle(); - subtitleLayout.setUserDefaultTextSize(); - View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + videoFrame = (ViewGroup) findViewById(R.id.exo_video_frame); + if (videoFrame != null) { + if (videoFrame instanceof AspectRatioFrameLayout) { + aspectRatioVideoFrame = (AspectRatioFrameLayout) videoFrame; + setResizeModeRaw(aspectRatioVideoFrame, resizeMode); + } else { + aspectRatioVideoFrame = null; + } + shutterView = Assertions.checkNotNull(videoFrame.findViewById(R.id.exo_shutter)); + if (surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context) + : new SurfaceView(context); + surfaceView.setLayoutParams(params); + videoFrame.addView(surfaceView, 0); + } else { + surfaceView = null; + } + } else { + aspectRatioVideoFrame = null; + shutterView = null; + surfaceView = null; + } - controller = new PlaybackControlView(context, attrs); - controller.setRewindIncrementMs(rewindMs); - controller.setFastForwardIncrementMs(fastForwardMs); - controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); - controller.hide(); - this.controllerShowTimeoutMs = controllerShowTimeoutMs; + artworkView = (ImageView) findViewById(R.id.exo_artwork); + this.useArtwork = useArtwork && artworkView != null; - ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); - int controllerIndex = parent.indexOfChild(controllerPlaceholder); - parent.removeView(controllerPlaceholder); - parent.addView(controller, controllerIndex); + subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles); + if (subtitleView != null) { + subtitleView.setUserDefaultStyle(); + subtitleView.setUserDefaultTextSize(); + } - View view = useTextureView ? new TextureView(context) : new SurfaceView(context); - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - view.setLayoutParams(params); - surfaceView = view; - layout.addView(surfaceView, 0); + PlaybackControlView controller = (PlaybackControlView) findViewById(R.id.exo_controller); + if (controller != null) { + controller.setRewindIncrementMs(rewindMs); + controller.setFastForwardIncrementMs(fastForwardMs); + } else { + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (controllerPlaceholder != null) { + // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit + // calls to set them. + controller = new PlaybackControlView(context, attrs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + } + } + this.controller = controller; + this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.useController = useController && controller != null; + hideController(); } /** @@ -150,6 +197,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (useController) { controller.setPlayer(player); } + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); @@ -160,21 +210,41 @@ public final class SimpleExoPlayerView extends FrameLayout { player.addListener(componentListener); player.setTextOutput(componentListener); maybeShowController(false); + updateForCurrentTrackSelections(); } else { - shutterView.setVisibility(VISIBLE); - controller.hide(); + hideController(); + hideArtwork(); } } /** - * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, - * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or - * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. + * Sets the resize mode. * * @param resizeMode The resize mode. */ - public void setResizeMode(int resizeMode) { - layout.setResizeMode(resizeMode); + public void setResizeMode(@ResizeMode int resizeMode) { + Assertions.checkState(aspectRatioVideoFrame != null); + aspectRatioVideoFrame.setResizeMode(resizeMode); + } + + /** + * Returns whether artwork is displayed if present in the media. + */ + public boolean getUseArtwork() { + return useArtwork; + } + + /** + * Sets whether artwork is displayed if present in the media. + * + * @param useArtwork Whether artwork is displayed. + */ + public void setUseArtwork(boolean useArtwork) { + Assertions.checkState(!useArtwork || artworkView != null); + if (this.useArtwork != useArtwork) { + this.useArtwork = useArtwork; + updateForCurrentTrackSelections(); + } } /** @@ -191,13 +261,14 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param useController Whether playback controls should be enabled. */ public void setUseController(boolean useController) { + Assertions.checkState(!useController || controller != null); if (this.useController == useController) { return; } this.useController = useController; if (useController) { controller.setPlayer(player); - } else { + } else if (controller != null) { controller.hide(); controller.setPlayer(null); } @@ -223,6 +294,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * the controller to remain visible indefinitely. */ public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { + Assertions.checkState(controller != null); this.controllerShowTimeoutMs = controllerShowTimeoutMs; } @@ -232,6 +304,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param listener The listener to be notified about visibility changes. */ public void setControllerVisibilityListener(PlaybackControlView.VisibilityListener listener) { + Assertions.checkState(controller != null); controller.setVisibilityListener(listener); } @@ -241,6 +314,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param rewindMs The rewind increment in milliseconds. */ public void setRewindIncrementMs(int rewindMs) { + Assertions.checkState(controller != null); controller.setRewindIncrementMs(rewindMs); } @@ -250,6 +324,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param fastForwardMs The fast forward increment in milliseconds. */ public void setFastForwardIncrementMs(int fastForwardMs) { + Assertions.checkState(controller != null); controller.setFastForwardIncrementMs(fastForwardMs); } @@ -304,6 +379,67 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + private void updateForCurrentTrackSelections() { + if (player == null) { + return; + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); + for (int i = 0; i < selections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; + } + } + // Video disabled so the shutter must be closed. + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + // Display artwork if enabled and available, else hide it. + if (useArtwork) { + for (int i = 0; i < selections.length; i++) { + TrackSelection selection = selections.get(i); + if (selection != null) { + for (int j = 0; j < selection.length(); j++) { + Metadata metadata = selection.getFormat(j).metadata; + if (metadata != null) { + for (int k = 0; k < metadata.length(); k++) { + Metadata.Entry metadataEntry = metadata.get(k); + if (metadataEntry instanceof ApicFrame) { + byte[] data = ((ApicFrame) metadataEntry).pictureData;; + artworkView.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)); + artworkView.setVisibility(VISIBLE); + return; + } + } + } + } + } + } + } + // Artwork disabled or unavailable. + hideArtwork(); + } + + private void hideArtwork() { + if (artworkView != null) { + artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. + artworkView.setVisibility(INVISIBLE); + } + } + + private void hideController() { + if (controller != null) { + controller.hide(); + } + } + + @SuppressWarnings("ResourceType") + private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { + aspectRatioFrame.setResizeMode(resizeMode); + } + private final class ComponentListener implements SimpleExoPlayer.VideoListener, TextRenderer.Output, ExoPlayer.EventListener { @@ -311,7 +447,9 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - subtitleLayout.onCues(cues); + if (subtitleView != null) { + subtitleView.onCues(cues); + } } // SimpleExoPlayer.VideoListener implementation @@ -319,23 +457,22 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - layout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height); + if (aspectRatioVideoFrame != null) { + float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; + aspectRatioVideoFrame.setAspectRatio(aspectRatio); + } } @Override public void onRenderedFirstFrame() { - shutterView.setVisibility(GONE); + if (shutterView != null) { + shutterView.setVisibility(INVISIBLE); + } } @Override public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - return; - } - } - // No enabled video renderers. Close the shutter. - shutterView.setVisibility(VISIBLE); + updateForCurrentTrackSelections(); } // ExoPlayer.EventListener implementation diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index f9de461b0c..b5f5022ca9 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -28,22 +28,22 @@ android:paddingTop="4dp" android:orientation="horizontal"> - - - - - - @@ -53,7 +53,7 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - - - - - - - + + diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 1cc22314c0..4be7cd590f 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -14,24 +14,33 @@ limitations under the License. --> - + + + + + + + + + - + + diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml index 9e810f1c2b..b768b75e89 100644 --- a/library/src/main/res/values/ids.xml +++ b/library/src/main/res/values/ids.xml @@ -14,13 +14,19 @@ limitations under the License. --> - - - - - - - - - + + + + + + + + + + + + + + + From 70cc98bb99e81d60ce691f1a4676e148af201347 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 27 Oct 2016 07:29:39 -0700 Subject: [PATCH 056/206] Fix broken log call ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137392736 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 81effe14b6..5ad28f9e72 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -371,7 +371,7 @@ import java.util.Locale; } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, - commentFrame.language, commentFrame.description, commentFrame.text)); + commentFrame.language, commentFrame.description)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; Log.d(TAG, prefix + String.format("%s", id3Frame.id)); From ee969b738e30f29bdd8fcdbaf5fe382b9c843eea Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Oct 2016 09:58:51 -0700 Subject: [PATCH 057/206] Load all exolist.json asset files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137406773 --- .../demo/SampleChooserActivity.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 95d42e0532..946181284f 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -43,6 +44,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -63,9 +65,21 @@ public class SampleChooserActivity extends Activity { if (dataUri != null) { uris = new String[] {dataUri}; } else { - uris = new String[] { - "asset:///media.exolist.json", - }; + ArrayList uriList = new ArrayList<>(); + AssetManager assetManager = getAssets(); + try { + for (String asset : assetManager.list("")) { + if (asset.endsWith(".exolist.json")) { + uriList.add("asset:///" + asset); + } + } + } catch (IOException e) { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + } + uris = new String[uriList.size()]; + uriList.toArray(uris); + Arrays.sort(uris); } SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); From daf7b948a113a1a5981de99f06a16810c73e4f14 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 31 Oct 2016 06:49:53 -0700 Subject: [PATCH 058/206] Fix buffering state when selecting VTT track with no cues ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137697023 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5c316a5653..1af0881f1a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -95,7 +95,7 @@ import java.util.concurrent.atomic.AtomicInteger; this.extractorNeedsInit = extractorNeedsInit; this.shouldSpliceIn = shouldSpliceIn; // Note: this.dataSource and dataSource may be different. - adjustedEndTimeUs = startTimeUs; + adjustedEndTimeUs = endTimeUs; this.isEncrypted = this.dataSource instanceof Aes128DataSource; uid = UID_SOURCE.getAndIncrement(); } From 488c2d82708fcd547a5a0b19bd56160914ed8567 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Oct 2016 07:10:31 -0700 Subject: [PATCH 059/206] Fixes for Issue #1962 - Use A/V tracks only for buffering position when available in ExtractorMediaPeriod. - Fix layering of exo_simple_player_view so that subtitles are above album art. The test stream provided has both. - Make album art in SimpleExoPlayer view respect the resize mode, like we do for video. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137698473 --- .../source/ExtractorMediaPeriod.java | 31 +++++-- .../exoplayer2/ui/AspectRatioFrameLayout.java | 8 +- .../exoplayer2/ui/SimpleExoPlayerView.java | 88 +++++++++++-------- .../res/layout/exo_simple_player_view.xml | 14 +-- library/src/main/res/values/attrs.xml | 1 + library/src/main/res/values/ids.xml | 2 +- 6 files changed, 93 insertions(+), 51 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 18de1b9df9..3b18a06c75 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.EOFException; import java.io.IOException; @@ -80,6 +81,8 @@ import java.io.IOException; private TrackGroupArray tracks; private long durationUs; private boolean[] trackEnabledStates; + private boolean[] trackIsAudioVideoFlags; + private boolean haveAudioVideoTracks; private long length; private long lastSeekPositionUs; @@ -259,11 +262,23 @@ import java.io.IOException; return C.TIME_END_OF_SOURCE; } else if (isPendingReset()) { return pendingResetPositionUs; - } else { - long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); - return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs - : largestQueuedTimestampUs; } + long largestQueuedTimestampUs; + if (haveAudioVideoTracks) { + // Ignore non-AV tracks, which may be sparse or poorly interleaved. + largestQueuedTimestampUs = Long.MAX_VALUE; + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + if (trackIsAudioVideoFlags[i]) { + largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, + sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + } + } + } else { + largestQueuedTimestampUs = getLargestQueuedTimestampUs(); + } + return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs + : largestQueuedTimestampUs; } @Override @@ -404,10 +419,16 @@ import java.io.IOException; } loadCondition.close(); TrackGroup[] trackArray = new TrackGroup[trackCount]; + trackIsAudioVideoFlags = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getUpstreamFormat()); + Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat(); + trackArray[i] = new TrackGroup(trackFormat); + String mimeType = trackFormat.sampleMimeType; + boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); + trackIsAudioVideoFlags[i] = isAudioVideo; + haveAudioVideoTracks |= isAudioVideo; } tracks = new TrackGroupArray(trackArray); prepared = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index f92903d65a..d3034a8bc8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -34,7 +34,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) public @interface ResizeMode {} /** @@ -49,6 +49,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. */ public static final int RESIZE_MODE_FIXED_HEIGHT = 2; + /** + * The specified aspect ratio is ignored. + */ + public static final int RESIZE_MODE_FILL = 3; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -109,7 +113,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (videoAspectRatio == 0) { + if (resizeMode == RESIZE_MODE_FILL || videoAspectRatio <= 0) { // Aspect ratio not set. return; } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index ecb9319c1d..90a15da12c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ui; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.AttributeSet; import android.view.KeyEvent; @@ -56,8 +57,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private static final int SURFACE_TYPE_SURFACE_VIEW = 1; private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; - private final ViewGroup videoFrame; - private final AspectRatioFrameLayout aspectRatioVideoFrame; + private final AspectRatioFrameLayout contentFrame; private final View shutterView; private final View surfaceView; private final ImageView artworkView; @@ -112,40 +112,39 @@ public final class SimpleExoPlayerView extends FrameLayout { LayoutInflater.from(context).inflate(playerLayoutId, this); componentListener = new ComponentListener(); - videoFrame = (ViewGroup) findViewById(R.id.exo_video_frame); - if (videoFrame != null) { - if (videoFrame instanceof AspectRatioFrameLayout) { - aspectRatioVideoFrame = (AspectRatioFrameLayout) videoFrame; - setResizeModeRaw(aspectRatioVideoFrame, resizeMode); - } else { - aspectRatioVideoFrame = null; - } - shutterView = Assertions.checkNotNull(videoFrame.findViewById(R.id.exo_shutter)); - if (surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context) - : new SurfaceView(context); - surfaceView.setLayoutParams(params); - videoFrame.addView(surfaceView, 0); - } else { - surfaceView = null; - } + // Content frame. + contentFrame = (AspectRatioFrameLayout) findViewById(R.id.exo_content_frame); + if (contentFrame != null) { + setResizeModeRaw(contentFrame, resizeMode); + } + + // Shutter view. + shutterView = findViewById(R.id.exo_shutter); + + // Create a surface view and insert it into the content frame, if there is one. + if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context) + : new SurfaceView(context); + surfaceView.setLayoutParams(params); + contentFrame.addView(surfaceView, 0); } else { - aspectRatioVideoFrame = null; - shutterView = null; surfaceView = null; } + // Artwork view. artworkView = (ImageView) findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; + // Subtitle view. subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles); if (subtitleView != null) { subtitleView.setUserDefaultStyle(); subtitleView.setUserDefaultTextSize(); } + // Playback control view. PlaybackControlView controller = (PlaybackControlView) findViewById(R.id.exo_controller); if (controller != null) { controller.setRewindIncrementMs(rewindMs); @@ -223,8 +222,8 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param resizeMode The resize mode. */ public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(aspectRatioVideoFrame != null); - aspectRatioVideoFrame.setResizeMode(resizeMode); + Assertions.checkState(contentFrame != null); + contentFrame.setResizeMode(resizeMode); } /** @@ -403,16 +402,8 @@ public final class SimpleExoPlayerView extends FrameLayout { if (selection != null) { for (int j = 0; j < selection.length(); j++) { Metadata metadata = selection.getFormat(j).metadata; - if (metadata != null) { - for (int k = 0; k < metadata.length(); k++) { - Metadata.Entry metadataEntry = metadata.get(k); - if (metadataEntry instanceof ApicFrame) { - byte[] data = ((ApicFrame) metadataEntry).pictureData;; - artworkView.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)); - artworkView.setVisibility(VISIBLE); - return; - } - } + if (metadata != null && setArtworkFromMetadata(metadata)) { + return; } } } @@ -422,6 +413,29 @@ public final class SimpleExoPlayerView extends FrameLayout { hideArtwork(); } + private boolean setArtworkFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof ApicFrame) { + byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); + if (bitmap != null) { + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + if (bitmapWidth > 0 && bitmapHeight > 0) { + if (contentFrame != null) { + contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); + } + artworkView.setImageBitmap(bitmap); + artworkView.setVisibility(VISIBLE); + return true; + } + } + } + } + return false; + } + private void hideArtwork() { if (artworkView != null) { artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. @@ -457,9 +471,9 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (aspectRatioVideoFrame != null) { + if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; - aspectRatioVideoFrame.setAspectRatio(aspectRatio); + contentFrame.setAspectRatio(aspectRatio); } } diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index f89eafdd58..c4f34ef285 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -17,27 +17,29 @@ android:layout_height="match_parent" android:layout_width="match_parent"> - + + + + - - diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 4be7cd590f..c3fc16495c 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -19,6 +19,7 @@ + diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml index b768b75e89..00f35d9098 100644 --- a/library/src/main/res/values/ids.xml +++ b/library/src/main/res/values/ids.xml @@ -14,7 +14,7 @@ limitations under the License. --> - + From 7b3690a0a62793f75ade69c484bb2d975362026e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Jul 2016 04:28:30 +0100 Subject: [PATCH 060/206] Fix onLoad* event media times for multi-period case ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137932100 --- .../AdaptiveMediaSourceEventListener.java | 41 +++++++++++++------ .../source/dash/DashMediaSource.java | 10 +++-- .../source/dash/manifest/Period.java | 6 +-- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 77b49f2be0..f97d4a1542 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.source; -import static com.google.android.exoplayer2.C.usToMs; - import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; @@ -174,10 +172,21 @@ public interface AdaptiveMediaSourceEventListener { private final Handler handler; private final AdaptiveMediaSourceEventListener listener; + private final long mediaTimeOffsetMs; public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { + this(handler, listener, 0); + } + + public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, + long mediaTimeOffsetMs) { this.handler = listener != null ? Assertions.checkNotNull(handler) : null; this.listener = listener; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { + return new EventDispatcher(handler, listener, mediaTimeOffsetMs); } public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { @@ -193,8 +202,8 @@ public interface AdaptiveMediaSourceEventListener { @Override public void run() { listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), - elapsedRealtimeMs); + trackSelectionData, adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); } }); } @@ -215,8 +224,8 @@ public interface AdaptiveMediaSourceEventListener { @Override public void run() { listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, usToMs(mediaStartTimeUs), - usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); + trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); } }); } @@ -237,8 +246,8 @@ public interface AdaptiveMediaSourceEventListener { @Override public void run() { listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, usToMs(mediaStartTimeUs), - usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); + trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); } }); } @@ -261,8 +270,9 @@ public interface AdaptiveMediaSourceEventListener { @Override public void run() { listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), - elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, wasCanceled); + trackSelectionData, adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, + error, wasCanceled); } }); } @@ -274,8 +284,8 @@ public interface AdaptiveMediaSourceEventListener { handler.post(new Runnable() { @Override public void run() { - listener.onUpstreamDiscarded(trackType, usToMs(mediaStartTimeUs), - usToMs(mediaEndTimeUs)); + listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs)); } }); } @@ -289,12 +299,17 @@ public interface AdaptiveMediaSourceEventListener { @Override public void run() { listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, usToMs(mediaTimeUs)); + trackSelectionData, adjustMediaTime(mediaTimeUs)); } }); } } + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index f22d1693a9..01341f2163 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -170,10 +170,12 @@ public final class DashMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + index, manifest, index, - chunkSourceFactory, minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffsetMs, loader, - allocator); + public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) { + EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( + manifest.getPeriod(periodIndex).startMs); + DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, + periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, + elapsedRealtimeOffsetMs, loader, allocator); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index 1d8b4fb300..269a63b7a9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -41,12 +41,12 @@ public class Period { /** * @param id The period identifier. May be null. - * @param start The start time of the period in milliseconds. + * @param startMs The start time of the period in milliseconds. * @param adaptationSets The adaptation sets belonging to the period. */ - public Period(String id, long start, List adaptationSets) { + public Period(String id, long startMs, List adaptationSets) { this.id = id; - this.startMs = start; + this.startMs = startMs; this.adaptationSets = Collections.unmodifiableList(adaptationSets); } From aaf38adc26e2d2333c67e5989c2d36a7b6627414 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 3 Nov 2016 03:55:12 -0700 Subject: [PATCH 061/206] Add support for HLS live seeking In order to expose the live window, it is necessary (unlike before) to refresh the live playlists being played periodically so as to know where the user can seek to. For this, the HlsPlaylistTracker is added, which is basically a map from HlsUrl's to playlist. One of the playlists involved in the playback will be chosen to define the live window. The playlist tracker it periodically. The rest of the playilst will be loaded lazily. N.B: This means that for VOD, playlists are not refreshed at all. There are three important features missing in this CL(that will be added in later CLs): * Blacklisting HlsUrls that point to resources that return 4xx response codes. As per [Internal: b/18948961]. * Allow loaded chunks to feed timestamps back to the tracker, to fix any drifting in live playlists. * Dinamically choose the HlsUrl that points to the playlist that defines the live window. Other features: -------------- The tracker can also be used for keeping track of discontinuities. In the case of single variant playlists, this is particularly useful. Might also work if there is a that the live playlists are aligned (but this is more like working around the issue, than actually solving it). For this, see [Internal: b/32166568] and [Internal: b/28985320]. Issue:#87 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138054302 --- .../playlist/HlsMediaPlaylistParserTest.java | 2 +- .../exoplayer2/source/hls/HlsChunkSource.java | 241 ++--------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 121 ++---- .../exoplayer2/source/hls/HlsMediaSource.java | 42 +- .../source/hls/HlsSampleStreamWrapper.java | 22 +- .../hls/playlist/HlsMasterPlaylist.java | 6 + .../source/hls/playlist/HlsMediaPlaylist.java | 44 +- .../hls/playlist/HlsPlaylistParser.java | 9 +- .../hls/playlist/HlsPlaylistTracker.java | 403 ++++++++++++++++++ 9 files changed, 558 insertions(+), 332 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index cd44a283a2..ac99760d87 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -74,7 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(8, mediaPlaylist.targetDurationSecs); assertEquals(3, mediaPlaylist.version); - assertEquals(false, mediaPlaylist.live); + assertEquals(true, mediaPlaylist.hasEndTag); List segments = mediaPlaylist.segments; assertNotNull(segments); assertEquals(5, segments.size()); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 7ef16f361d..351583a334 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.source.chunk.DataChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,7 +44,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; @@ -65,7 +64,7 @@ import java.util.Locale; } /** - * The chunk. + * The chunk to be loaded next. */ public Chunk chunk; @@ -75,9 +74,9 @@ import java.util.Locale; public boolean endOfStream; /** - * Milliseconds to wait before retrying. + * Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public long retryInMs; + public HlsMasterPlaylist.HlsUrl playlist; /** * Clears the holder. @@ -85,20 +84,11 @@ import java.util.Locale; public void clear() { chunk = null; endOfStream = false; - retryInMs = C.TIME_UNSET; + playlist = null; } } - /** - * The default time for which a media playlist should be blacklisted. - */ - public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; - /** - * Subtracted value to lookup position when switching between variants in live streams to avoid - * gaps in playback in case playlist drift apart. - */ - private static final double LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS = 2.0; private static final String AAC_FILE_EXTENSION = ".aac"; private static final String AC3_FILE_EXTENSION = ".ac3"; private static final String EC3_FILE_EXTENSION = ".ec3"; @@ -107,18 +97,13 @@ import java.util.Locale; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; - private final String baseUri; private final DataSource dataSource; - private final HlsPlaylistParser playlistParser; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsMasterPlaylist.HlsUrl[] variants; - private final HlsMediaPlaylist[] variantPlaylists; + private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; - private final long[] variantLastPlaylistLoadTimesMs; private byte[] scratchSpace; - private boolean live; - private long durationUs; private IOException fatalError; private HlsInitializationChunk lastLoadedInitializationChunk; @@ -133,22 +118,19 @@ import java.util.Locale; private TrackSelection trackSelection; /** - * @param baseUri The playlist's base uri. + * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. */ - public HlsChunkSource(String baseUri, HlsMasterPlaylist.HlsUrl[] variants, DataSource dataSource, - TimestampAdjusterProvider timestampAdjusterProvider) { - this.baseUri = baseUri; + public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsMasterPlaylist.HlsUrl[] variants, + DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + this.playlistTracker = playlistTracker; this.variants = variants; this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - playlistParser = new HlsPlaylistParser(); - variantPlaylists = new HlsMediaPlaylist[variants.length]; - variantLastPlaylistLoadTimesMs = new long[variants.length]; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; @@ -172,20 +154,6 @@ import java.util.Locale; } } - /** - * Returns whether this is a live playback. - */ - public boolean isLive() { - return live; - } - - /** - * Returns the duration of the source, or {@link C#TIME_UNSET} if the duration is unknown. - */ - public long getDurationUs() { - return durationUs; - } - /** * Returns the track group exposed by the source. */ @@ -214,8 +182,8 @@ import java.util.Locale; *

* If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but - * the end of the stream has not been reached, {@link HlsChunkHolder#retryInMs} is set to contain - * the amount of milliseconds to wait before retrying. + * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to + * contain the {@link HlsMasterPlaylist.HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position. If {@code previous} is null then this @@ -226,7 +194,6 @@ import java.util.Locale; public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); - // Use start time of the previous chunk rather than its end time because switching format will // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 @@ -235,67 +202,44 @@ import java.util.Locale; int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != newVariantIndex; - HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex]; + HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); if (mediaPlaylist == null) { - // We don't have the media playlist for the next variant. Request it now. - out.chunk = newMediaPlaylistChunk(newVariantIndex, trackSelection.getSelectionReason(), - trackSelection.getSelectionData()); + out.playlist = variants[newVariantIndex]; + // Retry when playlist is refreshed. return; } int chunkMediaSequence; - if (live) { - if (previous == null) { - // When playing a live stream, the starting chunk will be the third counting from the live - // edge. - chunkMediaSequence = Math.max(0, mediaPlaylist.segments.size() - 3) - + mediaPlaylist.mediaSequence; - // TODO: Bring this back for live window seeking. - // chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, - // true, true) + mediaPlaylist.mediaSequence; + if (previous == null || switchingVariant) { + long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; + if (targetPositionUs > mediaPlaylist.getEndTimeUs()) { + // If the playlist is too old to contain the chunk, we need to refresh it. + chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, - newVariantIndex); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true, + !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; + if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. newVariantIndex = oldVariantIndex; - mediaPlaylist = variantPlaylists[newVariantIndex]; - chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, - newVariantIndex); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - fatalError = new BehindLiveWindowException(); - return; - } + mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); + chunkMediaSequence = previous.getNextChunkIndex(); } } } else { - // Not live. - if (previous == null) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, - true, true) + mediaPlaylist.mediaSequence; - } else if (switchingVariant) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, - previous.startTimeUs, true, true) + mediaPlaylist.mediaSequence; - } else { - chunkMediaSequence = previous.getNextChunkIndex(); - } + chunkMediaSequence = previous.getNextChunkIndex(); + } + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { - if (!mediaPlaylist.live) { + if (mediaPlaylist.hasEndTag) { out.endOfStream = true; } else /* Live */ { - long msToRerequestLiveMediaPlaylist = msToRerequestLiveMediaPlaylist(newVariantIndex); - if (msToRerequestLiveMediaPlaylist <= 0) { - out.chunk = newMediaPlaylistChunk(newVariantIndex, - trackSelection.getSelectionReason(), trackSelection.getSelectionData()); - } else { - // 10 milliseconds are added to the wait to make sure the playlist is refreshed when - // getNextChunk() is called. - out.retryInMs = msToRerequestLiveMediaPlaylist + 10; - } + out.playlist = variants[newVariantIndex]; } return; } @@ -319,17 +263,9 @@ import java.util.Locale; } // Compute start and end times, and the sequence number of the next chunk. - long startTimeUs; - if (live) { - if (previous == null) { - startTimeUs = 0; - } else if (switchingVariant) { - startTimeUs = previous.getAdjustedStartTimeUs(); - } else { - startTimeUs = previous.getAdjustedEndTimeUs(); - } - } else /* Not live */ { - startTimeUs = segment.startTimeUs; + long startTimeUs = segment.startTimeUs; + if (previous != null && !switchingVariant) { + startTimeUs = previous.getAdjustedEndTimeUs(); } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); Format format = variants[newVariantIndex].format; @@ -424,52 +360,6 @@ import java.util.Locale; encryptionKey, encryptionIv); } - /** - * Returns the media sequence number of a chunk in a new variant for a live stream variant switch. - * - * @param previousChunkIndex The index of the last chunk in the old variant. - * @param oldVariantIndex The index of the old variant. - * @param newVariantIndex The index of the new variant. - * @return Media sequence number of the chunk to switch to in a live stream in the variant that - * corresponds to the given {@code newVariantIndex}. - */ - private int getLiveNextChunkSequenceNumber(int previousChunkIndex, int oldVariantIndex, - int newVariantIndex) { - if (oldVariantIndex == newVariantIndex) { - return previousChunkIndex + 1; - } - HlsMediaPlaylist oldMediaPlaylist = variantPlaylists[oldVariantIndex]; - HlsMediaPlaylist newMediaPlaylist = variantPlaylists[newVariantIndex]; - if (previousChunkIndex < oldMediaPlaylist.mediaSequence) { - // We have fallen behind the live window. - return newMediaPlaylist.mediaSequence - 1; - } - double offsetToLiveInstantSecs = 0; - for (int i = previousChunkIndex - oldMediaPlaylist.mediaSequence; - i < oldMediaPlaylist.segments.size(); i++) { - offsetToLiveInstantSecs += oldMediaPlaylist.segments.get(i).durationSecs; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - offsetToLiveInstantSecs += - (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[oldVariantIndex]) / 1000; - offsetToLiveInstantSecs += LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS; - offsetToLiveInstantSecs -= - (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[newVariantIndex]) / 1000; - if (offsetToLiveInstantSecs < 0) { - // The instant we are looking for is not contained in the playlist, we need it to be - // refreshed. - return newMediaPlaylist.mediaSequence + newMediaPlaylist.segments.size() + 1; - } - for (int i = newMediaPlaylist.segments.size() - 1; i >= 0; i--) { - offsetToLiveInstantSecs -= newMediaPlaylist.segments.get(i).durationSecs; - if (offsetToLiveInstantSecs < 0) { - return newMediaPlaylist.mediaSequence + i; - } - } - // We have fallen behind the live window. - return newMediaPlaylist.mediaSequence - 1; - } - /** * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this * source. @@ -479,10 +369,6 @@ import java.util.Locale; public void onChunkLoadCompleted(Chunk chunk) { if (chunk instanceof HlsInitializationChunk) { lastLoadedInitializationChunk = (HlsInitializationChunk) chunk; - } else if (chunk instanceof MediaPlaylistChunk) { - MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk; - scratchSpace = mediaPlaylistChunk.getDataHolder(); - setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult()); } else if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); @@ -519,24 +405,6 @@ import java.util.Locale; format); } - private long msToRerequestLiveMediaPlaylist(int variantIndex) { - HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; - long timeSinceLastMediaPlaylistLoadMs = - SystemClock.elapsedRealtime() - variantLastPlaylistLoadTimesMs[variantIndex]; - // Don't re-request media playlist more often than one-half of the target duration. - return (mediaPlaylist.targetDurationSecs * 1000) / 2 - timeSinceLastMediaPlaylistLoadMs; - } - - private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex, int trackSelectionReason, - Object trackSelectionData) { - Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); - DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNSET, null, - DataSpec.FLAG_ALLOW_GZIP); - return new MediaPlaylistChunk(dataSource, dataSpec, variants[variantIndex].format, - trackSelectionReason, trackSelectionData, scratchSpace, playlistParser, variantIndex, - mediaPlaylistUri); - } - private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); @@ -571,13 +439,6 @@ import java.util.Locale; encryptionIv = null; } - private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { - variantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); - variantPlaylists[variantIndex] = mediaPlaylist; - live |= mediaPlaylist.live; - durationUs = live ? C.TIME_UNSET : mediaPlaylist.durationUs; - } - // Private classes. /** @@ -626,38 +487,6 @@ import java.util.Locale; } - private static final class MediaPlaylistChunk extends DataChunk { - - public final int variantIndex; - - private final HlsPlaylistParser playlistParser; - private final Uri playlistUri; - - private HlsMediaPlaylist result; - - public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, - HlsPlaylistParser playlistParser, int variantIndex, - Uri playlistUri) { - super(dataSource, dataSpec, C.DATA_TYPE_MANIFEST, trackFormat, trackSelectionReason, - trackSelectionData, scratchSpace); - this.variantIndex = variantIndex; - this.playlistParser = playlistParser; - this.playlistUri = playlistUri; - } - - @Override - protected void consume(byte[] data, int limit) throws IOException { - result = (HlsMediaPlaylist) playlistParser.parse(playlistUri, - new ByteArrayInputStream(data, 0, limit)); - } - - public HlsMediaPlaylist getResult() { - return result; - } - - } - private static final class EncryptionKeyChunk extends DataChunk { public final String iv; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 598fa9b281..0c27b3df7d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -15,30 +15,22 @@ */ package com.google.android.exoplayer2.source.hls; -import android.net.Uri; import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -48,55 +40,41 @@ import java.util.List; /** * A {@link MediaPeriod} that loads an HLS stream. */ -/* package */ final class HlsMediaPeriod implements MediaPeriod, - Loader.Callback>, HlsSampleStreamWrapper.Callback { +public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, + HlsPlaylistTracker.PlaylistRefreshCallback { - private final Uri manifestUri; + private final HlsPlaylistTracker playlistTracker; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; - private final MediaSource.Listener sourceListener; private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final HlsPlaylistParser manifestParser; private final Handler continueLoadingHandler; private final Loader manifestFetcher; private final long preparePositionUs; - private final Runnable continueLoadingRunnable; private Callback callback; private int pendingPrepareCount; - private HlsPlaylist playlist; private boolean seenFirstTrackSelection; - private boolean isLive; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, - MediaSource.Listener sourceListener, Allocator allocator, + public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory, + int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, long positionUs) { - this.manifestUri = manifestUri; + this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; - this.sourceListener = sourceListener; this.allocator = allocator; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); - manifestParser = new HlsPlaylistParser(); continueLoadingHandler = new Handler(); manifestFetcher = new Loader("Loader:ManifestFetcher"); preparePositionUs = positionUs; - continueLoadingRunnable = new Runnable() { - @Override - public void run() { - callback.onContinueLoadingRequested(HlsMediaPeriod.this); - } - }; } public void release() { @@ -112,10 +90,7 @@ import java.util.List; @Override public void prepare(Callback callback) { this.callback = callback; - ParsingLoadable loadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); - long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + buildAndPrepareSampleStreamWrappers(); } @Override @@ -234,8 +209,6 @@ import java.util.List; @Override public long seekToUs(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = isLive ? 0 : positionUs; timestampAdjusterProvider.reset(); for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { sampleStreamWrapper.seekTo(positionUs); @@ -243,33 +216,6 @@ import java.util.List; return positionUs; } - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - playlist = loadable.getResult(); - buildAndPrepareSampleStreamWrappers(); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - // HlsSampleStreamWrapper.Callback implementation. @Override @@ -278,10 +224,6 @@ import java.util.List; return; } - // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. - long durationUs = sampleStreamWrappers[0].getDurationUs(); - isLive = sampleStreamWrappers[0].isLive(); - int totalTrackGroupCount = 0; for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length; @@ -296,16 +238,11 @@ import java.util.List; } trackGroups = new TrackGroupArray(trackGroupArray); callback.onPrepared(this); - - // TODO[playlists]: Calculate the window. - Timeline timeline = new SinglePeriodTimeline(durationUs, durationUs, 0, 0, !isLive, isLive); - sourceListener.onSourceInfoRefreshed(timeline, playlist); } @Override - public void onContinueLoadingRequiredInMs(final HlsSampleStreamWrapper sampleStreamWrapper, - long delayMs) { - continueLoadingHandler.postDelayed(continueLoadingRunnable, delayMs); + public void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl url) { + playlistTracker.refreshPlaylist(url, this); } @Override @@ -317,22 +254,24 @@ import java.util.List; callback.onContinueLoadingRequested(this); } + // PlaylistListener implementation. + + @Override + public void onPlaylistChanged() { + if (trackGroups != null) { + callback.onContinueLoadingRequested(this); + } else { + // Some of the wrappers were waiting for their media playlist to prepare. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } + } + } + // Internal methods. private void buildAndPrepareSampleStreamWrappers() { - String baseUri = playlist.baseUri; - if (playlist instanceof HlsMediaPlaylist) { - HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[] { - HlsMasterPlaylist.HlsUrl.createMediaPlaylistHlsUrl(playlist.baseUri)}; - sampleStreamWrappers = new HlsSampleStreamWrapper[] { - buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)}; - pendingPrepareCount = 1; - sampleStreamWrappers[0].continuePreparing(); - return; - } - - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - + HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); // Build the default stream wrapper. List selectedVariants = new ArrayList<>(masterPlaylist.variants); ArrayList definiteVideoVariants = new ArrayList<>(); @@ -367,7 +306,7 @@ import java.util.List; HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -375,7 +314,7 @@ import java.util.List; // Build audio stream wrappers. for (int i = 0; i < audioVariants.size(); i++) { HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); + new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -384,16 +323,16 @@ import java.util.List; for (int i = 0; i < subtitleVariants.size(); i++) { HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, - baseUri, new HlsMasterPlaylist.HlsUrl[] {url}, null, null); + new HlsMasterPlaylist.HlsUrl[] {url}, null, null); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, + private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsMasterPlaylist.HlsUrl[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, + HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, timestampAdjusterProvider); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index b8b6c033b3..6fd82df316 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -23,21 +23,26 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.Eve import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.List; /** * An HLS {@link MediaSource}. */ -public final class HlsMediaSource implements MediaSource { +public final class HlsMediaSource implements MediaSource, + HlsPlaylistTracker.PrimaryPlaylistListener { /** * The default minimum number of times to retry loading data prior to failing. */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri manifestUri; + private final HlsPlaylistTracker playlistTracker; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; @@ -53,29 +58,29 @@ public final class HlsMediaSource implements MediaSource { public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); + playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, + minLoadableRetryCount, this); } @Override public void prepareSource(MediaSource.Listener listener) { sourceListener = listener; - // TODO: Defer until the playlist has been loaded. - listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null); + playlistTracker.start(); } @Override - public void maybeThrowSourceInfoRefreshError() { - // Do nothing. + public void maybeThrowSourceInfoRefreshError() throws IOException { + playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); } @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { Assertions.checkArgument(index == 0); - return new HlsMediaPeriod(manifestUri, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, sourceListener, allocator, positionUs); + return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, + eventDispatcher, allocator, positionUs); } @Override @@ -85,7 +90,26 @@ public final class HlsMediaSource implements MediaSource { @Override public void releaseSource() { + playlistTracker.release(); sourceListener = null; } + @Override + public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { + SinglePeriodTimeline timeline; + if (playlistTracker.isLive()) { + // TODO: fix windowPositionInPeriodUs when playlist is empty. + long windowPositionInPeriodUs = playlist.getStartTimeUs(); + List segments = playlist.segments; + long windowDefaultStartPositionUs = segments.isEmpty() ? 0 + : segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs; + timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, + windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + } else /* not live */ { + timeline = new SinglePeriodTimeline(playlist.durationUs, playlist.durationUs, 0, 0, true, + false); + } + sourceListener.onSourceInfoRefreshed(timeline, playlist); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index fe756da0ef..c491dc9760 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; @@ -58,10 +59,10 @@ import java.util.LinkedList; void onPrepared(); /** - * Called to schedule a {@link #continueLoading(long)} call. + * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the + * given url changes. */ - void onContinueLoadingRequiredInMs(HlsSampleStreamWrapper sampleStreamSource, - long delayMs); + void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl playlistUrl); } @@ -164,14 +165,6 @@ import java.util.LinkedList; maybeThrowError(); } - public long getDurationUs() { - return chunkSource.getDurationUs(); - } - - public boolean isLive() { - return chunkSource.isLive(); - } - public TrackGroupArray getTrackGroups() { return trackGroups; } @@ -340,7 +333,7 @@ import java.util.LinkedList; nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; - long retryInMs = nextChunkHolder.retryInMs; + HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist; nextChunkHolder.clear(); if (endOfStream) { @@ -349,9 +342,8 @@ import java.util.LinkedList; } if (loadable == null) { - if (retryInMs != C.TIME_UNSET) { - Assertions.checkState(chunkSource.isLive()); - callback.onContinueLoadingRequiredInMs(this, retryInMs); + if (playlistToLoad != null) { + callback.onPlaylistRefreshRequired(playlistToLoad); } return false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index c0d4890b44..4aaec59f7d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -73,4 +73,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist { this.muxedCaptionFormat = muxedCaptionFormat; } + public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { + List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); + List emptyList = Collections.emptyList(); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 5aa0c8a3d8..177546d301 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.C; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -60,6 +62,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public int compareTo(Long startTimeUs) { return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); } + + public Segment copyWithStartTimeUs(long startTimeUs) { + return new Segment(url, durationSecs, discontinuitySequenceNumber, startTimeUs, isEncrypted, + encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); + } + } public static final String ENCRYPTION_METHOD_NONE = "NONE"; @@ -70,25 +78,51 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final int version; public final Segment initializationSegment; public final List segments; - public final boolean live; + public final boolean hasEndTag; public final long durationUs; public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, - boolean live, Segment initializationSegment, List segments) { + boolean hasEndTag, Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; this.targetDurationSecs = targetDurationSecs; this.version = version; - this.live = live; + this.hasEndTag = hasEndTag; this.initializationSegment = initializationSegment; - this.segments = segments; + this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { + Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND); + durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND) + - first.startTimeUs; } else { durationUs = 0; } } + public long getStartTimeUs() { + return segments.isEmpty() ? 0 : segments.get(0).startTimeUs; + } + + public long getEndTimeUs() { + return getStartTimeUs() + durationUs; + } + + public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) { + long startTimeOffsetUs = newStartTimeUs - getStartTimeUs(); + int segmentsSize = segments.size(); + List newSegments = new ArrayList<>(segmentsSize); + for (int i = 0; i < segmentsSize; i++) { + Segment segment = segments.get(i); + newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs)); + } + return copyWithSegments(newSegments); + } + + public HlsMediaPlaylist copyWithSegments(List segments) { + return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag, + initializationSegment, segments); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 21cc75765f..76606fad17 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -214,7 +213,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); @@ -298,11 +297,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser> { + + /** + * Listener for primary playlist changes. + */ + public interface PrimaryPlaylistListener { + + /** + * Called when the primary playlist changes. + * + * @param mediaPlaylist The primary playlist new snapshot. + */ + void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist); + + } + + /** + * Called when the playlist changes. + */ + public interface PlaylistRefreshCallback { + + /** + * Called when the target playlist changes. + */ + void onPlaylistChanged(); + + } + + /** + * Period for refreshing playlists. + */ + private static final long PLAYLIST_REFRESH_PERIOD_MS = 5000; + + private final Uri initialPlaylistUri; + private final DataSource.Factory dataSourceFactory; + private final HlsPlaylistParser playlistParser; + private final int minRetryCount; + private final IdentityHashMap playlistBundles; + private final Handler playlistRefreshHandler; + private final PrimaryPlaylistListener primaryPlaylistListener; + private final Loader initialPlaylistLoader; + private final EventDispatcher eventDispatcher; + + private HlsMasterPlaylist masterPlaylist; + private HlsUrl primaryHlsUrl; + private boolean isLive; + + /** + * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media + * playlist or a master playlist. + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param eventDispatcher A dispatcher to notify of events. + * @param minRetryCount The minimum number of times the load must be retried before blacklisting a + * playlist. + * @param primaryPlaylistListener A callback for the primary playlist change events. + */ + public HlsPlaylistTracker(Uri initialPlaylistUri, DataSource.Factory dataSourceFactory, + EventDispatcher eventDispatcher, int minRetryCount, + PrimaryPlaylistListener primaryPlaylistListener) { + this.initialPlaylistUri = initialPlaylistUri; + this.dataSourceFactory = dataSourceFactory; + this.eventDispatcher = eventDispatcher; + this.minRetryCount = minRetryCount; + this.primaryPlaylistListener = primaryPlaylistListener; + initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); + playlistParser = new HlsPlaylistParser(); + playlistBundles = new IdentityHashMap<>(); + playlistRefreshHandler = new Handler(); + } + + /** + * Starts tracking all the playlists related to the provided Uri. + */ + public void start() { + ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST, + playlistParser); + initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + } + + /** + * Returns the master playlist. + * + * @return The master playlist. Null if the initial playlist has yet to be loaded. + */ + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + /** + * Gets the most recent snapshot available of the playlist referred by the provided + * {@link HlsUrl}. + * + * @param url The {@link HlsUrl} corresponding to the requested media playlist. + * @return The most recent snapshot of the playlist referred by the provided {@link HlsUrl}. May + * be null if no snapshot has been loaded yet. + */ + public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { + return playlistBundles.get(url).latestPlaylistSnapshot; + } + + /** + * Releases the playlist tracker. + */ + public void release() { + initialPlaylistLoader.release(); + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistBundles.clear(); + } + + /** + * If the tracker is having trouble refreshing the primary playlist, this method throws the + * underlying error. Otherwise, does nothing. + * + * @throws IOException The underlying error. + */ + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + initialPlaylistLoader.maybeThrowError(); + if (primaryHlsUrl != null) { + playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError(); + } + } + + /** + * Triggers a playlist refresh and sets the callback to be called once the playlist referred by + * the provided {@link HlsUrl} changes. + * + * @param key The {@link HlsUrl} of the playlist to be refreshed. + * @param callback The callback. + */ + public void refreshPlaylist(HlsUrl key, PlaylistRefreshCallback callback) { + MediaPlaylistBundle bundle = playlistBundles.get(key); + bundle.setCallback(callback); + bundle.loadPlaylist(); + } + + /** + * Returns whether this is live content. + * + * @return True if the content is live. False otherwise. + */ + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + primaryHlsUrl = masterPlaylist.variants.get(0); + ArrayList urls = new ArrayList<>(); + urls.addAll(masterPlaylist.variants); + urls.addAll(masterPlaylist.audios); + urls.addAll(masterPlaylist.subtitles); + createBundles(urls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods. + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + HlsUrl url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(urls.get(i), bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + * @param isFirstSnapshot Whether this is the first snapshot for the given playlist. + * @return True if a refresh should be scheduled. + */ + private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot, + boolean isFirstSnapshot) { + if (url == primaryHlsUrl) { + if (isFirstSnapshot) { + isLive = !newSnapshot.hasEndTag; + } + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + // If the primary playlist is not the final one, we should schedule a refresh. + return !newSnapshot.hasEndTag; + } + return false; + } + + /** + * TODO: Allow chunks to feed adjusted timestamps back to the playlist tracker. + * TODO: Track discontinuities for media playlists that don't include the discontinuity number. + */ + private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist newPlaylist) { + HlsMediaPlaylist primaryPlaylistSnapshot = + playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot; + if (oldPlaylist == null) { + if (primaryPlaylistSnapshot == null) { + // Playback has just started so no adjustment is needed. + return newPlaylist; + } else { + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + } + } + int newSegmentsCount = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; + if (newSegmentsCount == 0 && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { + return oldPlaylist; + } + List oldSegments = oldPlaylist.segments; + int oldPlaylistSize = oldSegments.size(); + if (newSegmentsCount <= oldPlaylistSize) { + ArrayList newSegments = new ArrayList<>(); + // We can extrapolate the start time of new segments from the segments of the old snapshot. + int newPlaylistSize = newPlaylist.segments.size(); + for (int i = newSegmentsCount; i < oldPlaylistSize; i++) { + newSegments.add(oldSegments.get(i)); + } + HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); + for (int i = newPlaylistSize - newSegmentsCount; i < newPlaylistSize; i++) { + lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( + lastSegment.startTimeUs + (long) lastSegment.durationSecs * C.MICROS_PER_SECOND); + newSegments.add(lastSegment); + } + return newPlaylist.copyWithSegments(newSegments); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + } + } + + /** + * Holds all information related to a specific Media Playlist. + */ + private final class MediaPlaylistBundle implements Loader.Callback>, + Runnable { + + private final HlsUrl playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + private PlaylistRefreshCallback callback; + private HlsMediaPlaylist latestPlaylistSnapshot; + + public MediaPlaylistBundle(HlsUrl playlistUrl) { + this(playlistUrl, null); + } + + public MediaPlaylistBundle(HlsUrl playlistUrl, HlsMediaPlaylist initialSnapshot) { + this.playlistUrl = playlistUrl; + latestPlaylistSnapshot = initialSnapshot; + mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(), + UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, + playlistParser); + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + if (!mediaPlaylistLoader.isLoading()) { + mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + } + } + + public void setCallback(PlaylistRefreshCallback callback) { + this.callback = callback; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + processLoadedPlaylist((HlsMediaPlaylist) loadable.getResult()); + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + // TODO: Add support for playlist blacklisting in response to server error codes. + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Runnable implementation. + + @Override + public void run() { + loadPlaylist(); + } + + // Internal methods. + + private void processLoadedPlaylist(HlsMediaPlaylist loadedMediaPlaylist) { + HlsMediaPlaylist oldPlaylist = latestPlaylistSnapshot; + latestPlaylistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist); + boolean shouldScheduleRefresh; + if (oldPlaylist != latestPlaylistSnapshot) { + if (callback != null) { + callback.onPlaylistChanged(); + callback = null; + } + shouldScheduleRefresh = onPlaylistUpdated(playlistUrl, latestPlaylistSnapshot, + oldPlaylist == null); + } else { + shouldScheduleRefresh = !loadedMediaPlaylist.hasEndTag; + } + if (shouldScheduleRefresh) { + playlistRefreshHandler.postDelayed(this, PLAYLIST_REFRESH_PERIOD_MS); + } + } + + } + +} From d9421f4fb9d91aab39e5b5ac908e1bac8f241297 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 3 Nov 2016 08:47:24 -0700 Subject: [PATCH 062/206] Fixed issue when VOD-style period is present in a dynamic manifest A VOD-style period in a dynamic manifest would result in a NullPointerException. Also fix another issue in which an unrecognized mime type would also result in NullPointerException. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138075137 --- .../exoplayer2/source/dash/DashMediaSource.java | 10 +++++++--- .../exoplayer2/text/SubtitleDecoderFactory.java | 3 +++ .../google/android/exoplayer2/util/MimeTypes.java | 13 ++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 01341f2163..ec794a534d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -424,9 +424,13 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex index = period.adaptationSets.get(videoAdaptationSetIndex).representations.get(0).getIndex(); - int segmentNum = index.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); - windowDefaultStartPositionUs = - defaultStartPositionUs - defaultStartPositionInPeriodUs + index.getTimeUs(segmentNum); + if (index != null) { + int segmentNum = index.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); + windowDefaultStartPositionUs = + defaultStartPositionUs - defaultStartPositionInPeriodUs + index.getTimeUs(segmentNum); + } else { + windowDefaultStartPositionUs = defaultStartPositionUs; + } } else { windowDefaultStartPositionUs = defaultStartPositionUs; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 2cbc1ab622..d1afbe86a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -81,6 +81,9 @@ public interface SubtitleDecoderFactory { } private Class getDecoderClass(String mimeType) { + if (mimeType == null) { + return null; + } try { switch (mimeType) { case MimeTypes.TEXT_VTT: diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e289a5bac1..fb91b17cc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -84,7 +84,7 @@ public final class MimeTypes { * @return Whether the top level type is audio. */ public static boolean isAudio(String mimeType) { - return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO); + return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); } /** @@ -94,7 +94,7 @@ public final class MimeTypes { * @return Whether the top level type is video. */ public static boolean isVideo(String mimeType) { - return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO); + return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } /** @@ -104,7 +104,7 @@ public final class MimeTypes { * @return Whether the top level type is text. */ public static boolean isText(String mimeType) { - return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT); + return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); } /** @@ -114,7 +114,7 @@ public final class MimeTypes { * @return Whether the top level type is application. */ public static boolean isApplication(String mimeType) { - return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION); + return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } @@ -237,9 +237,12 @@ public final class MimeTypes { * Returns the top-level type of {@code mimeType}. * * @param mimeType The mimeType whose top-level type is required. - * @return The top-level type. + * @return The top-level type, or null if the mimeType is null. */ private static String getTopLevelType(String mimeType) { + if (mimeType == null) { + return null; + } int indexOfSlash = mimeType.indexOf('/'); if (indexOfSlash == -1) { throw new IllegalArgumentException("Invalid mime type: " + mimeType); From 6c7ead5d0cadb81dc46d221a66ff9e68fd150a76 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Nov 2016 10:00:24 -0700 Subject: [PATCH 063/206] Document all the ways our new UI components can be customised. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138082864 --- .../exoplayer2/ui/PlaybackControlView.java | 134 +++++++++++++++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 132 ++++++++++++++--- .../res/layout/exo_playback_control_view.xml | 4 +- library/src/main/res/values/ids.xml | 5 +- 4 files changed, 233 insertions(+), 42 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 03ef4e2f33..29772dcc89 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -38,17 +38,109 @@ import java.util.Formatter; import java.util.Locale; /** - * A view to control video playback of an {@link ExoPlayer}. - * - * By setting the view attribute {@code controller_layout_id} a layout resource to use can - * be customized. All views are optional but if the buttons should have an appropriate logic - * assigned, the id of the views in the layout have to match the expected ids as follows: + * A view for controlling {@link ExoPlayer} instances. + *

+ * A PlaybackControlView can be customized by setting attributes (or calling corresponding methods), + * overriding the view's layout file or by specifying a custom view layout file, as outlined below. * + *

Attributes

+ * The following attributes can be set on a PlaybackControlView when used in a layout XML file: + *

*

    - *
  • Playback: {@code exo_play}, {@code exo_pause}, {@code exo_ffwd}, {@code exo_rew}.
  • - *
  • Progress: {@code exo_progress}, {@code exo_time_current}, {@code exo_time}.
  • - *
  • Playlist navigation: {@code exo_previous}, {@code exo_next}.
  • + *
  • {@code show_timeout} - The time between the last user interaction and the controls + * being automatically hidden, in milliseconds. Use zero if the controls should not + * automatically timeout. + *
      + *
    • Corresponding method: {@link #setShowTimeoutMs(int)}
    • + *
    • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
    • + *
    + *
  • + *
  • {@code rewind_increment} - The duration of the rewind applied when the user taps the + * rewind button, in milliseconds. Use zero to disable the rewind button. + *
      + *
    • Corresponding method: {@link #setRewindIncrementMs(int)}
    • + *
    • Default: {@link #DEFAULT_REWIND_MS}
    • + *
    + *
  • + *
  • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. + *
      + *
    • Corresponding method: {@link #setFastForwardIncrementMs(int)}
    • + *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS}
    • + *
    + *
  • + *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See + * below for more details. + *
      + *
    • Corresponding method: None
    • + *
    • Default: {@code R.id.exo_playback_control_view}
    • + *
    + *
  • *
+ * + *

Overriding the layout file

+ * To customize the layout of PlaybackControlView throughout your app, or just for certain + * configurations, you can define {@code exo_playback_control_view.xml} layout files in your + * application {@code res/layout*} directories. These layouts will override the one provided by the + * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and + * binds its children by looking for the following ids: + *

+ *

    + *
  • {@code exo_play} - The play button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_pause} - The pause button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_ffwd} - The fast forward button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_rew} - The rewind button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_prev} - The previous track button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_next} - The next track button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_position} - Text view displaying the current playback position. + *
      + *
    • Type: {@link TextView}
    • + *
    + *
  • + *
  • {@code exo_duration} - Text view displaying the current media duration. + *
      + *
    • Type: {@link TextView}
    • + *
    + *
  • + *
  • {@code exo_progress} - Seek bar that's updated during playback and allows seeking. + *
      + *
    • Type: {@link SeekBar}
    • + *
    + *
  • + *
+ *

+ * All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

Specifying a custom layout file

+ * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of + * PlaybackControlView throughout your application. It's also possible to customize the layout for a + * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} + * attribute on a PlaybackControlView. This will cause the specified layout to be inflated instead + * of {@code exo_playback_control_view.xml} for only the instance on which the attribute is set. */ public class PlaybackControlView extends FrameLayout { @@ -78,8 +170,8 @@ public class PlaybackControlView extends FrameLayout { private final View pauseButton; private final View fastForwardButton; private final View rewindButton; - private final TextView time; - private final TextView timeCurrent; + private final TextView durationView; + private final TextView positionView; private final SeekBar progressBar; private final StringBuilder formatBuilder; private final Formatter formatter; @@ -144,8 +236,8 @@ public class PlaybackControlView extends FrameLayout { componentListener = new ComponentListener(); LayoutInflater.from(context).inflate(controllerLayoutId, this); - time = (TextView) findViewById(R.id.exo_time); - timeCurrent = (TextView) findViewById(R.id.exo_time_current); + durationView = (TextView) findViewById(R.id.exo_duration); + positionView = (TextView) findViewById(R.id.exo_position); progressBar = (SeekBar) findViewById(R.id.exo_progress); if (progressBar != null) { progressBar.setOnSeekBarChangeListener(componentListener); @@ -215,7 +307,8 @@ public class PlaybackControlView extends FrameLayout { /** * Sets the rewind increment in milliseconds. * - * @param rewindMs The rewind increment in milliseconds. + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. */ public void setRewindIncrementMs(int rewindMs) { this.rewindMs = rewindMs; @@ -225,7 +318,8 @@ public class PlaybackControlView extends FrameLayout { /** * Sets the fast forward increment in milliseconds. * - * @param fastForwardMs The fast forward increment in milliseconds. + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. */ public void setFastForwardIncrementMs(int fastForwardMs) { this.fastForwardMs = fastForwardMs; @@ -355,11 +449,11 @@ public class PlaybackControlView extends FrameLayout { } long duration = player == null ? 0 : player.getDuration(); long position = player == null ? 0 : player.getCurrentPosition(); - if (time != null) { - time.setText(stringForTime(duration)); + if (durationView != null) { + durationView.setText(stringForTime(duration)); } - if (timeCurrent != null && !dragging) { - timeCurrent.setText(stringForTime(position)); + if (positionView != null && !dragging) { + positionView.setText(stringForTime(position)); } if (progressBar != null) { @@ -541,8 +635,8 @@ public class PlaybackControlView extends FrameLayout { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser && timeCurrent != null) { - timeCurrent.setText(stringForTime(positionValue(progress))); + if (fromUser && positionView != null) { + positionView.setText(stringForTime(positionValue(progress))); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 90a15da12c..d494ab2a10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -48,7 +48,110 @@ import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** - * Displays a video stream. + * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and + * album art during playback, and displays playback controls using a {@link PlaybackControlView}. + *

+ * A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding methods), + * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

Attributes

+ * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: + *

+ *

    + *
  • {@code use_artwork} - Whether artwork is used if available in audio streams. + *
      + *
    • Corresponding method: {@link #setUseArtwork(boolean)}
    • + *
    • Default: {@code true}
    • + *
    + *
  • + *
  • {@code use_controller} - Whether playback controls are displayed. + *
      + *
    • Corresponding method: {@link #setUseController(boolean)}
    • + *
    • Default: {@code true}
    • + *
    + *
  • + *
  • {@code resize_mode} - Controls how video and album art is resized within the view. + * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. + *
      + *
    • Corresponding method: {@link #setResizeMode(int)}
    • + *
    • Default: {@code fit}
    • + *
    + *
  • + *
  • {@code surface_type} - The type of surface view used for video playbacks. Valid + * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} + * is recommended for audio only applications, since creating the surface can be expensive. + * Using {@code surface_view} is recommended for video applications. + *
      + *
    • Corresponding method: None
    • + *
    • Default: {@code surface_view}
    • + *
    + *
  • + *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below + * for more details. + *
      + *
    • Corresponding method: None
    • + *
    • Default: {@code R.id.exo_simple_player_view}
    • + *
    + *
  • {@code controller_layout_id} - Specifies the id of the layout resource to be + * inflated by the child {@link PlaybackControlView}. See below for more details. + *
      + *
    • Corresponding method: None
    • + *
    • Default: {@code R.id.exo_playback_control_view}
    • + *
    + *
  • All attributes that can be set on a {@link PlaybackControlView} can also be set on a + * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView}. + *
  • + *
+ * + *

Overriding the layout file

+ * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain + * configurations, you can define {@code exo_simple_player_view.xml} layout files in your + * application {@code res/layout*} directories. These layouts will override the one provided by the + * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and + * binds its children by looking for the following ids: + *

+ *

    + *
  • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video + * or album art of the media being played, and the configured {@code resize_mode}. The video + * surface view is inflated into this frame as its first child. + *
      + *
    • Type: {@link AspectRatioFrameLayout}
    • + *
    + *
  • + *
  • {@code exo_shutter} - A view that's made visible when video should be hidden. This + * view is typically an opaque view that covers the video surface view, thereby obscuring it + * when visible. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
  • {@code exo_subtitles} - Displays subtitles. + *
      + *
    • Type: {@link SubtitleView}
    • + *
    + *
  • + *
  • {@code exo_artwork} - Displays album art. + *
      + *
    • Type: {@link ImageView}
    • + *
    + *
  • + *
  • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated + * {@link PlaybackControlView}. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • + *
+ *

+ * All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

Specifying a custom layout file

+ * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of + * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a + * single instance in a layout file. This is achieved by setting the {@code player_layout_id} + * attribute on a SimpleExoPlayerView. This will cause the specified layout to be inflated instead + * of {@code exo_simple_player_view.xml} for only the instance on which the attribute is set. */ @TargetApi(16) public final class SimpleExoPlayerView extends FrameLayout { @@ -145,24 +248,19 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Playback control view. - PlaybackControlView controller = (PlaybackControlView) findViewById(R.id.exo_controller); - if (controller != null) { - controller.setRewindIncrementMs(rewindMs); - controller.setFastForwardIncrementMs(fastForwardMs); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (controllerPlaceholder != null) { + // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit + // calls to set them. + this.controller = new PlaybackControlView(context, attrs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); } else { - View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (controllerPlaceholder != null) { - // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit - // calls to set them. - controller = new PlaybackControlView(context, attrs); - controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); - ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); - int controllerIndex = parent.indexOfChild(controllerPlaceholder); - parent.removeView(controllerPlaceholder); - parent.addView(controller, controllerIndex); - } + this.controller = null; } - this.controller = controller; this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.useController = useController && controller != null; hideController(); diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index b5f5022ca9..2cf8b132ac 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -53,7 +53,7 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - - - - - + +
From e2081f40fb01657ec32a023cae6dcfb8b29bcb00 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Mon, 18 Jul 2016 16:51:25 +0100 Subject: [PATCH 064/206] move baseUrl from segments to representations: V2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138136090 --- .../assets/dash/sample_mpd_3_segment_template | 38 +++++++++ .../dash/manifest/DashManifestParserTest.java | 28 +++++++ .../source/dash/manifest/RangedUriTest.java | 64 ++++++++------- .../dash/manifest/RepresentationTest.java | 7 +- .../source/dash/DashWrappingSegmentIndex.java | 4 +- .../source/dash/DefaultDashChunkSource.java | 11 +-- .../dash/manifest/DashManifestParser.java | 77 ++++++++++--------- .../source/dash/manifest/RangedUri.java | 49 ++++++------ .../source/dash/manifest/Representation.java | 39 ++++++---- .../source/dash/manifest/SegmentBase.java | 26 ++----- 10 files changed, 204 insertions(+), 139 deletions(-) create mode 100644 library/src/androidTest/assets/dash/sample_mpd_3_segment_template diff --git a/library/src/androidTest/assets/dash/sample_mpd_3_segment_template b/library/src/androidTest/assets/dash/sample_mpd_3_segment_template new file mode 100644 index 0000000000..a9147b54df --- /dev/null +++ b/library/src/androidTest/assets/dash/sample_mpd_3_segment_template @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + + + diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 66ee298daf..460d3bb04e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -28,6 +28,8 @@ public class DashManifestParserTest extends InstrumentationTestCase { private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1"; private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "dash/sample_mpd_2_unknown_mime_type"; + private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = + "dash/sample_mpd_3_segment_template"; /** * Simple test to ensure the sample manifests parse without any exceptions being thrown. @@ -40,4 +42,30 @@ public class DashManifestParserTest extends InstrumentationTestCase { TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); } + public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); + + assertEquals(1, mpd.getPeriodCount()); + + Period period = mpd.getPeriod(0); + assertNotNull(period); + assertEquals(2, period.adaptationSets.size()); + + for (AdaptationSet adaptationSet : period.adaptationSets) { + assertNotNull(adaptationSet); + for (Representation representation : adaptationSet.representations) { + if (representation instanceof Representation.MultiSegmentRepresentation) { + Representation.MultiSegmentRepresentation multiSegmentRepresentation = + (Representation.MultiSegmentRepresentation) representation; + int firstSegmentIndex = multiSegmentRepresentation.getFirstSegmentNum(); + RangedUri uri = multiSegmentRepresentation.getSegmentUrl(firstSegmentIndex); + assertTrue(uri.resolveUriString(representation.baseUrl).contains( + "redirector.googlevideo.com")); + } + } + } + } + } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java index 59e1c14a33..fd559381fa 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java @@ -23,56 +23,64 @@ import junit.framework.TestCase; */ public class RangedUriTest extends TestCase { - private static final String FULL_URI = "http://www.test.com/path/file.ext"; + private static final String BASE_URI = "http://www.test.com/"; + private static final String PARTIAL_URI = "path/file.ext"; + private static final String FULL_URI = BASE_URI + PARTIAL_URI; public void testMerge() { - RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); - RangedUri rangeB = new RangedUri(null, FULL_URI, 10, 10); - RangedUri expected = new RangedUri(null, FULL_URI, 0, 20); - assertMerge(rangeA, rangeB, expected); + RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); + RangedUri expected = new RangedUri(FULL_URI, 0, 20); + assertMerge(rangeA, rangeB, expected, null); } public void testMergeUnbounded() { - RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); - RangedUri rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET); - RangedUri expected = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET); - assertMerge(rangeA, rangeB, expected); + RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET); + RangedUri expected = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET); + assertMerge(rangeA, rangeB, expected, null); } public void testNonMerge() { // A and B do not overlap, so should not merge - RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); - RangedUri rangeB = new RangedUri(null, FULL_URI, 11, 10); - assertNonMerge(rangeA, rangeB); + RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(FULL_URI, 11, 10); + assertNonMerge(rangeA, rangeB, null); // A and B do not overlap, so should not merge - rangeA = new RangedUri(null, FULL_URI, 0, 10); - rangeB = new RangedUri(null, FULL_URI, 11, C.LENGTH_UNSET); - assertNonMerge(rangeA, rangeB); + rangeA = new RangedUri(FULL_URI, 0, 10); + rangeB = new RangedUri(FULL_URI, 11, C.LENGTH_UNSET); + assertNonMerge(rangeA, rangeB, null); // A and B are bounded but overlap, so should not merge - rangeA = new RangedUri(null, FULL_URI, 0, 11); - rangeB = new RangedUri(null, FULL_URI, 10, 10); - assertNonMerge(rangeA, rangeB); + rangeA = new RangedUri(FULL_URI, 0, 11); + rangeB = new RangedUri(FULL_URI, 10, 10); + assertNonMerge(rangeA, rangeB, null); // A and B overlap due to unboundedness, so should not merge - rangeA = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET); - rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET); - assertNonMerge(rangeA, rangeB); - + rangeA = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET); + rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET); + assertNonMerge(rangeA, rangeB, null); } - private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected) { - RangedUri merged = rangeA.attemptMerge(rangeB); + public void testMergeWithBaseUri() { + RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10); + RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); + RangedUri expected = new RangedUri(FULL_URI, 0, 20); + assertMerge(rangeA, rangeB, expected, BASE_URI); + } + + private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected, String baseUrl) { + RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl); assertEquals(expected, merged); - merged = rangeB.attemptMerge(rangeA); + merged = rangeB.attemptMerge(rangeA, baseUrl); assertEquals(expected, merged); } - private void assertNonMerge(RangedUri rangeA, RangedUri rangeB) { - RangedUri merged = rangeA.attemptMerge(rangeB); + private void assertNonMerge(RangedUri rangeA, RangedUri rangeB, String baseUrl) { + RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl); assertNull(merged); - merged = rangeB.attemptMerge(rangeA); + merged = rangeB.attemptMerge(rangeA, baseUrl); assertNull(merged); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java index 681969ffa2..008cd0e556 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java @@ -27,16 +27,17 @@ public class RepresentationTest extends TestCase { public void testGetCacheKey() { String uri = "http://www.google.com"; - SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1); + SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1); Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null, MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); - Representation representation = Representation.newInstance("test_stream_1", 3, format, base); + Representation representation = Representation.newInstance("test_stream_1", 3, format, uri, + base); assertEquals("test_stream_1.0.3", representation.getCacheKey()); format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null, MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT, - format, base); + format, uri, base); assertEquals("test_stream_1.150.-1", representation.getCacheKey()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 716c9ad844..9e48bc2c79 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; /* package */ final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; - private final String uri; /** * @param chunkIndex The {@link ChunkIndex} to wrap. @@ -33,7 +32,6 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; */ public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) { this.chunkIndex = chunkIndex; - this.uri = uri; } @Override @@ -58,7 +56,7 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; @Override public RangedUri getSegmentUrl(int segmentNum) { - return new RangedUri(uri, null, chunkIndex.offsets[segmentNum], chunkIndex.sizes[segmentNum]); + return new RangedUri(null, chunkIndex.offsets[segmentNum], chunkIndex.sizes[segmentNum]); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index a7c7389b2b..83b8724170 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -288,18 +288,19 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { RangedUri requestUri; + String baseUrl = representationHolder.representation.baseUrl; if (initializationUri != null) { // It's common for initialization and index data to be stored adjacently. Attempt to merge // the two requests together to request both at once. - requestUri = initializationUri.attemptMerge(indexUri); + requestUri = initializationUri.attemptMerge(indexUri, baseUrl); if (requestUri == null) { requestUri = initializationUri; } } else { requestUri = indexUri; } - DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, - representationHolder.representation.getCacheKey()); + DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start, + requestUri.length, representationHolder.representation.getCacheKey()); return new InitializationChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } @@ -311,8 +312,8 @@ public class DefaultDashChunkSource implements DashChunkSource { long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); - DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, - representation.getCacheKey()); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(representation.baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); if (representationHolder.extractorWrapper == null) { return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index b2f0ae6f98..fdfe8cb4b3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -205,11 +205,11 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { - segmentBase = parseSegmentBase(xpp, baseUrl, null); + segmentBase = parseSegmentBase(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, null); + segmentBase = parseSegmentList(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, null); + segmentBase = parseSegmentTemplate(xpp, null); } } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); @@ -263,11 +263,11 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { - segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); + segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -390,11 +390,11 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { - segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); + segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { SchemeData contentProtection = parseContentProtection(xpp); if (contentProtection != null) { @@ -407,7 +407,7 @@ public class DashManifestParser extends DefaultHandler audioSamplingRate, bandwidth, adaptationSetLanguage, codecs); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl); - return new RepresentationInfo(format, segmentBase, drmSchemeDatas); + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -441,13 +441,13 @@ public class DashManifestParser extends DefaultHandler format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); } return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, - representationInfo.segmentBase); + representationInfo.baseUrl, representationInfo.segmentBase); } // SegmentBase, SegmentList and SegmentTemplate parsing. - protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, String baseUrl, - SingleSegmentBase parent) throws XmlPullParserException, IOException { + protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, SingleSegmentBase parent) + throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -466,21 +466,21 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { - initialization = parseInitialization(xpp, baseUrl); + initialization = parseInitialization(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase")); - return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, - indexStart, indexLength); + return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); } protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, - long presentationTimeOffset, String baseUrl, long indexStart, long indexLength) { - return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, - indexStart, indexLength); + long presentationTimeOffset, long indexStart, long indexLength) { + return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent) + protected SegmentList parseSegmentList(XmlPullParser xpp, SegmentList parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -496,14 +496,14 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { - initialization = parseInitialization(xpp, baseUrl); + initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { timeline = parseSegmentTimeline(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { if (segments == null) { segments = new ArrayList<>(); } - segments.add(parseSegmentUrl(xpp, baseUrl)); + segments.add(parseSegmentUrl(xpp)); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList")); @@ -524,8 +524,8 @@ public class DashManifestParser extends DefaultHandler startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl, - SegmentTemplate parent) throws XmlPullParserException, IOException { + protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent) + throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", parent != null ? parent.presentationTimeOffset : 0); @@ -542,7 +542,7 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { - initialization = parseInitialization(xpp, baseUrl); + initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { timeline = parseSegmentTimeline(xpp); } @@ -554,15 +554,15 @@ public class DashManifestParser extends DefaultHandler } return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); + startNumber, duration, timeline, initializationTemplate, mediaTemplate); } protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, int startNumber, long duration, List timeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate, String baseUrl) { + UrlTemplate mediaTemplate) { return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); + startNumber, duration, timeline, initializationTemplate, mediaTemplate); } protected List parseSegmentTimeline(XmlPullParser xpp) @@ -597,15 +597,15 @@ public class DashManifestParser extends DefaultHandler return defaultValue; } - protected RangedUri parseInitialization(XmlPullParser xpp, String baseUrl) { - return parseRangedUrl(xpp, baseUrl, "sourceURL", "range"); + protected RangedUri parseInitialization(XmlPullParser xpp) { + return parseRangedUrl(xpp, "sourceURL", "range"); } - protected RangedUri parseSegmentUrl(XmlPullParser xpp, String baseUrl) { - return parseRangedUrl(xpp, baseUrl, "media", "mediaRange"); + protected RangedUri parseSegmentUrl(XmlPullParser xpp) { + return parseRangedUrl(xpp, "media", "mediaRange"); } - protected RangedUri parseRangedUrl(XmlPullParser xpp, String baseUrl, String urlAttribute, + protected RangedUri parseRangedUrl(XmlPullParser xpp, String urlAttribute, String rangeAttribute) { String urlText = xpp.getAttributeValue(null, urlAttribute); long rangeStart = 0; @@ -618,12 +618,11 @@ public class DashManifestParser extends DefaultHandler rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1; } } - return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength); + return buildRangedUri(urlText, rangeStart, rangeLength); } - protected RangedUri buildRangedUri(String baseUrl, String urlText, long rangeStart, - long rangeLength) { - return new RangedUri(baseUrl, urlText, rangeStart, rangeLength); + protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) { + return new RangedUri(urlText, rangeStart, rangeLength); } // AudioChannelConfiguration parsing. @@ -788,12 +787,14 @@ public class DashManifestParser extends DefaultHandler private static final class RepresentationInfo { public final Format format; + public final String baseUrl; public final SegmentBase segmentBase; public final ArrayList drmSchemeDatas; - public RepresentationInfo(Format format, SegmentBase segmentBase, + public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, ArrayList drmSchemeDatas) { this.format = format; + this.baseUrl = baseUrl; this.segmentBase = segmentBase; this.drmSchemeDatas = drmSchemeDatas; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index 1668526b22..c2a64718df 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -17,11 +17,10 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; /** - * Defines a range of data located at a {@link Uri}. + * Defines a range of data located at a reference uri. */ public final class RangedUri { @@ -35,12 +34,6 @@ public final class RangedUri { */ public final long length; - // The URI is stored internally in two parts: reference URI and a base URI to use when - // resolving it. This helps optimize memory usage in the same way that DASH manifests allow many - // URLs to be expressed concisely in the form of a single BaseURL and many relative paths. Note - // that this optimization relies on the same object being passed as the base URI to many - // instances of this class. - private final String baseUri; private final String referenceUri; private int hashCode; @@ -48,57 +41,59 @@ public final class RangedUri { /** * Constructs an ranged uri. * - * @param baseUri A uri that can form the base of the uri defined by the instance. - * @param referenceUri A reference uri that should be resolved with respect to {@code baseUri}. + * @param referenceUri The reference uri. * @param start The (zero based) index of the first byte of the range. * @param length The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is * unbounded. */ - public RangedUri(String baseUri, String referenceUri, long start, long length) { - Assertions.checkArgument(baseUri != null || referenceUri != null); - this.baseUri = baseUri; - this.referenceUri = referenceUri; + public RangedUri(String referenceUri, long start, long length) { + this.referenceUri = referenceUri == null ? "" : referenceUri; this.start = start; this.length = length; } /** - * Returns the {@link Uri} represented by the instance. + * Returns the resolved {@link Uri} represented by the instance. * + * @param baseUri The base Uri. * @return The {@link Uri} represented by the instance. */ - public Uri getUri() { + public Uri resolveUri(String baseUri) { return UriUtil.resolveToUri(baseUri, referenceUri); } /** - * Returns the uri represented by the instance as a string. + * Returns the resolved uri represented by the instance as a string. * + * @param baseUri The base Uri. * @return The uri represented by the instance. */ - public String getUriString() { + public String resolveUriString(String baseUri) { return UriUtil.resolve(baseUri, referenceUri); } /** - * Attempts to merge this {@link RangedUri} with another. + * Attempts to merge this {@link RangedUri} with another and an optional common base uri. *

- * A merge is successful if both instances define the same {@link Uri}, and if one starts the byte - * after the other ends, forming a contiguous region with no overlap. + * A merge is successful if both instances define the same {@link Uri} after resolution with the + * base uri, and if one starts the byte after the other ends, forming a contiguous region with + * no overlap. *

* If {@code other} is null then the merge is considered unsuccessful, and null is returned. * * @param other The {@link RangedUri} to merge. + * @param baseUri The optional base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ - public RangedUri attemptMerge(RangedUri other) { - if (other == null || !getUriString().equals(other.getUriString())) { + public RangedUri attemptMerge(RangedUri other, String baseUri) { + final String resolvedUri = resolveUriString(baseUri); + if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) { return null; } else if (length != C.LENGTH_UNSET && start + length == other.start) { - return new RangedUri(baseUri, referenceUri, start, + return new RangedUri(resolvedUri, start, other.length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length + other.length); } else if (other.length != C.LENGTH_UNSET && other.start + other.length == start) { - return new RangedUri(baseUri, referenceUri, other.start, + return new RangedUri(resolvedUri, other.start, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : other.length + length); } else { return null; @@ -111,7 +106,7 @@ public final class RangedUri { int result = 17; result = 31 * result + (int) start; result = 31 * result + (int) length; - result = 31 * result + getUriString().hashCode(); + result = 31 * result + referenceUri.hashCode(); hashCode = result; } return hashCode; @@ -128,7 +123,7 @@ public final class RangedUri { RangedUri other = (RangedUri) obj; return this.start == other.start && this.length == other.length - && getUriString().equals(other.getUriString()); + && referenceUri.equals(other.referenceUri); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 6ebd69e29b..f52727c1a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -52,6 +52,10 @@ public abstract class Representation { * The format of the representation. */ public final Format format; + /** + * The base URL of the representation. + */ + public final String baseUrl; /** * The offset of the presentation timestamps in the media stream relative to media time. */ @@ -65,12 +69,13 @@ public abstract class Representation { * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. + * @param baseUrl The base URL. * @param segmentBase A segment base element for the representation. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - SegmentBase segmentBase) { - return newInstance(contentId, revisionId, format, segmentBase, null); + String baseUrl, SegmentBase segmentBase) { + return newInstance(contentId, revisionId, format, baseUrl, segmentBase, null); } /** @@ -79,18 +84,19 @@ public abstract class Representation { * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. + * @param baseUrl The base URL of the representation. * @param segmentBase A segment base element for the representation. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - SegmentBase segmentBase, String customCacheKey) { + String baseUrl, SegmentBase segmentBase, String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { - return new SingleSegmentRepresentation(contentId, revisionId, format, + return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, (SingleSegmentBase) segmentBase, customCacheKey, C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { - return new MultiSegmentRepresentation(contentId, revisionId, format, + return new MultiSegmentRepresentation(contentId, revisionId, format, baseUrl, (MultiSegmentBase) segmentBase); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " @@ -98,11 +104,12 @@ public abstract class Representation { } } - private Representation(String contentId, long revisionId, Format format, + private Representation(String contentId, long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; + this.baseUrl = baseUrl; initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } @@ -166,26 +173,27 @@ public abstract class Representation { public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, long indexStart, long indexEnd, String customCacheKey, long contentLength) { - RangedUri rangedUri = new RangedUri(uri, null, initializationStart, + RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); - SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart, + SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1); return new SingleSegmentRepresentation(contentId, revisionId, - format, segmentBase, customCacheKey, contentLength); + format, uri, segmentBase, customCacheKey, contentLength); } /** * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. + * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, - SingleSegmentBase segmentBase, String customCacheKey, long contentLength) { - super(contentId, revisionId, format, segmentBase); - this.uri = Uri.parse(segmentBase.uri); + String baseUrl, SingleSegmentBase segmentBase, String customCacheKey, long contentLength) { + super(contentId, revisionId, format, baseUrl, segmentBase); + this.uri = Uri.parse(baseUrl); this.indexUri = segmentBase.getIndex(); this.cacheKey = customCacheKey != null ? customCacheKey : contentId != null ? contentId + "." + format.id + "." + revisionId : null; @@ -193,7 +201,7 @@ public abstract class Representation { // If we have an index uri then the index is defined externally, and we shouldn't return one // directly. If we don't, then we can't do better than an index defining a single segment. segmentIndex = indexUri != null ? null - : new SingleSegmentIndex(new RangedUri(segmentBase.uri, null, 0, contentLength)); + : new SingleSegmentIndex(new RangedUri(null, 0, contentLength)); } @Override @@ -225,11 +233,12 @@ public abstract class Representation { * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. + * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - MultiSegmentBase segmentBase) { - super(contentId, revisionId, format, segmentBase); + String baseUrl, MultiSegmentBase segmentBase) { + super(contentId, revisionId, format, baseUrl, segmentBase); this.segmentBase = segmentBase; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index dec626c326..966f88e5bc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -65,11 +65,6 @@ public abstract class SegmentBase { */ public static class SingleSegmentBase extends SegmentBase { - /** - * The uri of the segment. - */ - public final String uri; - /* package */ final long indexStart; /* package */ final long indexLength; @@ -79,27 +74,22 @@ public abstract class SegmentBase { * @param timescale The timescale in units per second. * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. - * @param uri The uri of the segment. * @param indexStart The byte offset of the index data in the segment. * @param indexLength The length of the index data in bytes. */ public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - String uri, long indexStart, long indexLength) { + long indexStart, long indexLength) { super(initialization, timescale, presentationTimeOffset); - this.uri = uri; this.indexStart = indexStart; this.indexLength = indexLength; } - /** - * @param uri The uri of the segment. - */ public SingleSegmentBase(String uri) { - this(null, 1, 0, uri, 0, 0); + this(null, 1, 0, 0, 0); } public RangedUri getIndex() { - return indexLength <= 0 ? null : new RangedUri(uri, null, indexStart, indexLength); + return indexLength <= 0 ? null : new RangedUri(null, indexStart, indexLength); } } @@ -279,8 +269,6 @@ public abstract class SegmentBase { /* package */ final UrlTemplate initializationTemplate; /* package */ final UrlTemplate mediaTemplate; - private final String baseUrl; - /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data * exists. The value of this parameter is ignored if {@code initializationTemplate} is @@ -299,16 +287,14 @@ public abstract class SegmentBase { * such data exists. If non-null then the {@code initialization} parameter is ignored. If * null then {@code initialization} will be used. * @param mediaTemplate A template defining the location of each media segment. - * @param baseUrl A url to use as the base for relative urls generated by the templates. */ public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, int startNumber, long duration, List segmentTimeline, - UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, String baseUrl) { + UrlTemplate initializationTemplate, UrlTemplate mediaTemplate) { super(initialization, timescale, presentationTimeOffset, startNumber, duration, segmentTimeline); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; - this.baseUrl = baseUrl; } @Override @@ -316,7 +302,7 @@ public abstract class SegmentBase { if (initializationTemplate != null) { String urlString = initializationTemplate.buildUri(representation.format.id, 0, representation.format.bitrate, 0); - return new RangedUri(baseUrl, urlString, 0, C.LENGTH_UNSET); + return new RangedUri(urlString, 0, C.LENGTH_UNSET); } else { return super.getInitialization(representation); } @@ -332,7 +318,7 @@ public abstract class SegmentBase { } String uriString = mediaTemplate.buildUri(representation.format.id, sequenceNumber, representation.format.bitrate, time); - return new RangedUri(baseUrl, uriString, 0, C.LENGTH_UNSET); + return new RangedUri(uriString, 0, C.LENGTH_UNSET); } @Override From 3e2cb9f89ab5061b6cc1e67fceedbdb3075eafe3 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Nov 2016 04:07:57 -0700 Subject: [PATCH 065/206] Fix incorrect renderer reset enable position handlePeriodPrepared ->setPlayingPeriodHolder ->enableRenderers passes rendererPositionUs to renderer.enable(), but this value is not set correctly. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138176114 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 6d8f3af0c3..76e60c27b6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1069,12 +1069,12 @@ import java.io.IOException; if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; + resetRendererPosition(readingPeriodHolder.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); if (playbackInfo.startPositionUs == C.TIME_UNSET) { // Update the playback info when seeking to a default position. playbackInfo = new PlaybackInfo(playingPeriodHolder.index, playingPeriodHolder.startPositionUs); - resetRendererPosition(playbackInfo.startPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } @@ -1116,8 +1116,7 @@ import java.io.IOException; } } - private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) - throws ExoPlaybackException { + private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { From a5a2bc89f48a6ceff49f6a52ca93b5cfe8360cab Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Nov 2016 07:03:51 -0700 Subject: [PATCH 066/206] Push window->period seekTo conversions into ExoPlayerImplInternal ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138185910 --- .../android/exoplayer2/ExoPlayerImpl.java | 43 +++-------- .../exoplayer2/ExoPlayerImplInternal.java | 71 ++++++++++++------- 2 files changed, 55 insertions(+), 59 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2e0502c019..7c973ca995 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -48,7 +48,6 @@ import java.util.concurrent.CopyOnWriteArraySet; private final Timeline.Period period; private boolean tracksSelected; - private boolean pendingInitialSeek; private boolean playWhenReady; private int playbackState; private int pendingSeekAcks; @@ -168,17 +167,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void seekToDefaultPosition(int windowIndex) { - if (timeline == null) { - maskingWindowIndex = windowIndex; - maskingWindowPositionMs = C.TIME_UNSET; - pendingInitialSeek = true; - } else { - Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); - pendingSeekAcks++; - maskingWindowIndex = windowIndex; - maskingWindowPositionMs = 0; - internalPlayer.seekTo(timeline.getWindow(windowIndex, window).firstPeriodIndex, C.TIME_UNSET); - } + seekTo(windowIndex, C.TIME_UNSET); } @Override @@ -188,27 +177,14 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void seekTo(int windowIndex, long positionMs) { + pendingSeekAcks++; + maskingWindowIndex = windowIndex; if (positionMs == C.TIME_UNSET) { - seekToDefaultPosition(windowIndex); - } else if (timeline == null) { - maskingWindowIndex = windowIndex; - maskingWindowPositionMs = positionMs; - pendingInitialSeek = true; + maskingWindowPositionMs = 0; + internalPlayer.seekTo(windowIndex, C.TIME_UNSET); } else { - Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); - pendingSeekAcks++; - maskingWindowIndex = windowIndex; maskingWindowPositionMs = positionMs; - timeline.getWindow(windowIndex, window); - int periodIndex = window.firstPeriodIndex; - long periodPositionMs = window.getPositionInFirstPeriodMs() + positionMs; - long periodDurationMs = timeline.getPeriod(periodIndex, period).getDurationMs(); - while (periodDurationMs != C.TIME_UNSET && periodPositionMs >= periodDurationMs - && periodIndex < window.lastPeriodIndex) { - periodPositionMs -= periodDurationMs; - periodDurationMs = timeline.getPeriod(++periodIndex, period).getDurationMs(); - } - internalPlayer.seekTo(periodIndex, C.msToUs(periodPositionMs)); + internalPlayer.seekTo(windowIndex, C.msToUs(positionMs)); for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); } @@ -349,7 +325,8 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { - if (--pendingSeekAcks == 0) { + pendingSeekAcks -= msg.arg1; + if (pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -371,10 +348,6 @@ import java.util.concurrent.CopyOnWriteArraySet; Pair timelineAndManifest = (Pair) msg.obj; timeline = timelineAndManifest.first; manifest = timelineAndManifest.second; - if (pendingInitialSeek) { - pendingInitialSeek = false; - seekTo(maskingWindowIndex, maskingWindowPositionMs); - } for (EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 76e60c27b6..7d24c1b0d5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -139,6 +139,9 @@ import java.io.IOException; private int customMessagesProcessed; private long elapsedRealtimeUs; + private int pendingInitialSeekCount; + private int pendingSeekWindowIndex; + private long pendingSeekWindowPositionUs; private long rendererPositionUs; private boolean isTimelineReady; @@ -189,8 +192,8 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } - public void seekTo(int periodIndex, long positionUs) { - handler.obtainMessage(MSG_SEEK_TO, periodIndex, 0, positionUs).sendToTarget(); + public void seekTo(int windowIndex, long positionUs) { + handler.obtainMessage(MSG_SEEK_TO, windowIndex, 0, positionUs).sendToTarget(); } public void stop() { @@ -510,16 +513,19 @@ import java.io.IOException; } } - private void seekToInternal(int periodIndex, long periodPositionUs) throws ExoPlaybackException { - try { - if (periodPositionUs == C.TIME_UNSET && timeline != null - && periodIndex < timeline.getPeriodCount()) { - // We know about the window, so seek to its default initial position now. - Pair defaultPosition = getDefaultPosition(periodIndex); - periodIndex = defaultPosition.first; - periodPositionUs = defaultPosition.second; - } + private void seekToInternal(int windowIndex, long positionUs) throws ExoPlaybackException { + if (timeline == null) { + pendingInitialSeekCount++; + pendingSeekWindowIndex = windowIndex; + pendingSeekWindowPositionUs = positionUs; + return; + } + Pair periodPosition = getPeriodPosition(windowIndex, positionUs); + int periodIndex = periodPosition.first; + long periodPositionUs = periodPosition.second; + + try { if (periodIndex == playbackInfo.periodIndex && ((periodPositionUs == C.TIME_UNSET && playbackInfo.positionUs == C.TIME_UNSET) || ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000)))) { @@ -529,7 +535,7 @@ import java.io.IOException; periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); } } @@ -819,6 +825,15 @@ import java.io.IOException; Timeline oldTimeline = this.timeline; this.timeline = timelineAndManifest.first; + if (pendingInitialSeekCount > 0) { + Pair periodPosition = getPeriodPosition(pendingSeekWindowIndex, + pendingSeekWindowPositionUs); + playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); + eventHandler.obtainMessage(MSG_SEEK_ACK, pendingInitialSeekCount, 0, playbackInfo) + .sendToTarget(); + pendingInitialSeekCount = 0; + } + // Update the loaded periods to take into account the new timeline. if (playingPeriodHolder != null) { int index = timeline.getIndexOfPeriod(playingPeriodHolder.uid); @@ -922,7 +937,8 @@ import java.io.IOException; loadingPeriodHolder = null; // Find the default initial position in the window and seek to it. - Pair defaultPosition = getDefaultPosition(newPeriodIndex); + Pair defaultPosition = getPeriodPosition( + timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); newPeriodIndex = defaultPosition.first; long newPlayingPositionUs = defaultPosition.second; @@ -930,17 +946,24 @@ import java.io.IOException; eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } - private Pair getDefaultPosition(int periodIndex) { - timeline.getPeriod(periodIndex, period); - timeline.getWindow(period.windowIndex, window); - periodIndex = window.firstPeriodIndex; + /** + * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). + * + * @param windowIndex The window index. + * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default + * start position. + * @return The corresponding (periodIndex, periodPositionUs). + */ + private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { + timeline.getWindow(windowIndex, window); + int periodIndex = window.firstPeriodIndex; long periodPositionUs = window.getPositionInFirstPeriodUs() - + window.getDefaultPositionUs(); - timeline.getPeriod(periodIndex, period); - while (periodIndex < window.lastPeriodIndex - && periodPositionUs > period.getDurationMs()) { - periodPositionUs -= period.getDurationUs(); - timeline.getPeriod(periodIndex++, period); + + (windowPositionUs == C.TIME_UNSET ? window.getDefaultPositionUs() : windowPositionUs); + long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); } return Pair.create(periodIndex, periodPositionUs); } @@ -970,7 +993,7 @@ import java.io.IOException; if (periodStartPositionUs == C.TIME_UNSET) { // This is the first period of a new window or we don't have a start position, so seek to // the default position for the window. - Pair defaultPosition = getDefaultPosition(newLoadingPeriodIndex); + Pair defaultPosition = getPeriodPosition(windowIndex, C.TIME_UNSET); newLoadingPeriodIndex = defaultPosition.first; periodStartPositionUs = defaultPosition.second; } From 992cfdecc250d20129516a922536e007fe371a4b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 4 Nov 2016 10:48:14 -0700 Subject: [PATCH 067/206] Feed timestamps from loaded chunks back to the playlist tracker This is the first step towards allowing discontinuities in the playlist tracking. Also changed durationSecs for durationUs in MediaPlaylist.Segment. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138207732 --- .../playlist/HlsMediaPlaylistParserTest.java | 11 ++--- .../exoplayer2/source/hls/HlsChunkSource.java | 11 +++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 14 ++++-- .../source/hls/playlist/HlsMediaPlaylist.java | 20 +++----- .../hls/playlist/HlsPlaylistParser.java | 13 ++--- .../hls/playlist/HlsPlaylistTracker.java | 49 ++++++++++++++++++- 6 files changed, 84 insertions(+), 34 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index ac99760d87..67ec907d61 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -72,7 +72,6 @@ public class HlsMediaPlaylistParserTest extends TestCase { HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; assertEquals(2679, mediaPlaylist.mediaSequence); - assertEquals(8, mediaPlaylist.targetDurationSecs); assertEquals(3, mediaPlaylist.version); assertEquals(true, mediaPlaylist.hasEndTag); List segments = mediaPlaylist.segments; @@ -80,7 +79,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals(5, segments.size()); assertEquals(4, segments.get(0).discontinuitySequenceNumber); - assertEquals(7.975, segments.get(0).durationSecs); + assertEquals(7975000, segments.get(0).durationUs); assertEquals(false, segments.get(0).isEncrypted); assertEquals(null, segments.get(0).encryptionKeyUri); assertEquals(null, segments.get(0).encryptionIV); @@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); assertEquals(4, segments.get(1).discontinuitySequenceNumber); - assertEquals(7.975, segments.get(1).durationSecs); + assertEquals(7975000, segments.get(1).durationUs); assertEquals(true, segments.get(1).isEncrypted); assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); assertEquals("0x1566B", segments.get(1).encryptionIV); @@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); assertEquals(4, segments.get(2).discontinuitySequenceNumber); - assertEquals(7.941, segments.get(2).durationSecs); + assertEquals(7941000, segments.get(2).durationUs); assertEquals(false, segments.get(2).isEncrypted); assertEquals(null, segments.get(2).encryptionKeyUri); assertEquals(null, segments.get(2).encryptionIV); @@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); assertEquals(5, segments.get(3).discontinuitySequenceNumber); - assertEquals(7.975, segments.get(3).durationSecs); + assertEquals(7975000, segments.get(3).durationUs); assertEquals(true, segments.get(3).isEncrypted); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); // 0xA7A == 2682. @@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); assertEquals(5, segments.get(4).discontinuitySequenceNumber); - assertEquals(7.975, segments.get(4).durationSecs); + assertEquals(7975000, segments.get(4).durationUs); assertEquals(true, segments.get(4).isEncrypted); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); // 0xA7B == 2683. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 351583a334..cbef07f6fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -267,7 +267,7 @@ import java.util.Locale; if (previous != null && !switchingVariant) { startTimeUs = previous.getAdjustedEndTimeUs(); } - long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); + long endTimeUs = startTimeUs + segment.durationUs; Format format = variants[newVariantIndex].format; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); @@ -322,7 +322,7 @@ import java.util.Locale; // This flag ensures the change of pid between streams does not affect the sample queues. @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = 0; - String codecs = variants[newVariantIndex].format.codecs; + String codecs = format.codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really // exist. If we know from the codec attribute that they don't exist, then we can @@ -353,7 +353,7 @@ import java.util.Locale; // Configure the data source and spec for the chunk. DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, format, + out.chunk = new HlsMediaChunk(dataSource, dataSpec, variants[newVariantIndex], trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, extractor, extractorNeedsInit, switchingVariant, @@ -367,6 +367,11 @@ import java.util.Locale; * @param chunk The chunk whose load has been completed. */ public void onChunkLoadCompleted(Chunk chunk) { + if (chunk instanceof HlsMediaChunk) { + HlsMediaChunk mediaChunk = (HlsMediaChunk) chunk; + playlistTracker.onChunkLoaded(mediaChunk.hlsUrl, mediaChunk.chunkIndex, + mediaChunk.getAdjustedStartTimeUs()); + } if (chunk instanceof HlsInitializationChunk) { lastLoadedInitializationChunk = (HlsInitializationChunk) chunk; } else if (chunk instanceof EncryptionKeyChunk) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 1af0881f1a..25435022c5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -15,12 +15,12 @@ */ package com.google.android.exoplayer2.source.hls; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; @@ -49,6 +49,11 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final Extractor extractor; + /** + * The url of the playlist from which this chunk was obtained. + */ + public final HlsUrl hlsUrl; + private final boolean isEncrypted; private final boolean extractorNeedsInit; private final boolean shouldSpliceIn; @@ -64,7 +69,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. - * @param trackFormat See {@link #trackFormat}. + * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. @@ -81,13 +86,14 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ - public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, + public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, Extractor extractor, boolean extractorNeedsInit, boolean shouldSpliceIn, byte[] encryptionKey, byte[] encryptionIv) { - super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trackFormat, + super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.hlsUrl = hlsUrl; this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 177546d301..2962d656be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public static final class Segment implements Comparable { public final String url; - public final double durationSecs; + public final long durationUs; public final int discontinuitySequenceNumber; public final long startTimeUs; public final boolean isEncrypted; @@ -44,11 +44,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); } - public Segment(String uri, double durationSecs, int discontinuitySequenceNumber, + public Segment(String uri, long durationUs, int discontinuitySequenceNumber, long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = uri; - this.durationSecs = durationSecs; + this.durationUs = durationUs; this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.startTimeUs = startTimeUs; this.isEncrypted = isEncrypted; @@ -64,28 +64,23 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } public Segment copyWithStartTimeUs(long startTimeUs) { - return new Segment(url, durationSecs, discontinuitySequenceNumber, startTimeUs, isEncrypted, + return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted, encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); } } - public static final String ENCRYPTION_METHOD_NONE = "NONE"; - public static final String ENCRYPTION_METHOD_AES_128 = "AES-128"; - public final int mediaSequence; - public final int targetDurationSecs; public final int version; public final Segment initializationSegment; public final List segments; public final boolean hasEndTag; public final long durationUs; - public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, + public HlsMediaPlaylist(String baseUri, int mediaSequence, int version, boolean hasEndTag, Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; - this.targetDurationSecs = targetDurationSecs; this.version = version; this.hasEndTag = hasEndTag; this.initializationSegment = initializationSegment; @@ -94,8 +89,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (!segments.isEmpty()) { Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND) - - first.startTimeUs; + durationUs = last.startTimeUs + last.durationUs - first.startTimeUs; } else { durationUs = 0; } @@ -121,7 +115,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } public HlsMediaPlaylist copyWithSegments(List segments) { - return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag, + return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag, initializationSegment, segments); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 76606fad17..420500615a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -217,7 +217,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); - double segmentDurationSecs = 0.0; + long segmentDurationUs = 0; int discontinuitySequenceNumber = 0; long segmentStartTimeUs = 0; long segmentByteRangeOffset = 0; @@ -252,7 +252,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(latestPlaylistSnapshot.segments); + int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence; + if (indexOfChunk < 0) { + return; + } + Segment actualSegment = segments.get(indexOfChunk); + long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs); + if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { + return; + } + segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs)); + // Propagate the adjustment backwards. + for (int i = indexOfChunk - 1; i >= 0; i--) { + Segment segment = segments.get(i); + segments.set(i, + segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs)); + } + // Propagate the adjustment forward. + int segmentsSize = segments.size(); + for (int i = indexOfChunk + 1; i < segmentsSize; i++) { + Segment segment = segments.get(i); + segments.set(i, + segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs)); + } + latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments); + } + // Loader.Callback implementation. @Override From ff77d1e72cc3836abf0a724b1791fa39176b7472 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 7 Nov 2016 03:39:15 -0800 Subject: [PATCH 068/206] Add index file to hold header information for cached content. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138373878 --- .../upstream/cache/CacheDataSourceTest.java | 15 +- .../upstream/cache/CacheSpanTest.java | 79 ------ .../cache/CachedContentIndexTest.java | 171 +++++++++++ .../upstream/cache/SimpleCacheSpanTest.java | 155 ++++++++++ .../upstream/cache/SimpleCacheTest.java | 43 ++- .../android/exoplayer2/util/UtilTest.java | 15 - .../exoplayer2/upstream/cache/Cache.java | 4 +- .../upstream/cache/CacheDataSource.java | 11 +- .../exoplayer2/upstream/cache/CacheSpan.java | 92 ++---- .../upstream/cache/CachedContent.java | 199 +++++++++++++ .../upstream/cache/CachedContentIndex.java | 265 ++++++++++++++++++ .../upstream/cache/SimpleCache.java | 261 +++++++---------- .../upstream/cache/SimpleCacheSpan.java | 128 +++++++++ .../android/exoplayer2/util/AtomicFile.java | 213 ++++++++++++++ .../google/android/exoplayer2/util/Util.java | 46 +-- .../android/exoplayer2/testutil/TestUtil.java | 9 + 16 files changed, 1295 insertions(+), 411 deletions(-) delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index d46458db2b..c0d9570d7a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; - import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - // Create a temporary folder - cacheDir = File.createTempFile("CacheDataSourceTest", null); - assertTrue(cacheDir.delete()); - assertTrue(cacheDir.mkdir()); - + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); } @@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); - assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE), - cacheDir.listFiles().length); + File[] files = cacheDir.listFiles(); + for (File file : files) { + if (file.getName().endsWith(SimpleCacheSpan.SUFFIX)) { + assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); + } + } } public void testCacheAndRead() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java deleted file mode 100644 index 38008c814e..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016 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.upstream.cache; - -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.File; -import java.util.Random; -import junit.framework.TestCase; - -/** - * Unit tests for {@link CacheSpan}. - */ -public class CacheSpanTest extends TestCase { - - public void testCacheFile() throws Exception { - assertCacheSpan(new File("parent"), "key", 0, 0); - assertCacheSpan(new File("parent/"), "key", 1, 2); - assertCacheSpan(new File("parent"), "<>:\"/\\|?*%", 1, 2); - assertCacheSpan(new File("/"), "key", 1, 2); - - assertNullCacheSpan(new File("parent"), "", 1, 2); - assertNullCacheSpan(new File("parent"), "key", -1, 2); - assertNullCacheSpan(new File("parent"), "key", 1, -2); - - assertNotNull(CacheSpan.createCacheEntry(new File("/asd%aa.1.2.v2.exo"))); - assertNull(CacheSpan.createCacheEntry(new File("/asd%za.1.2.v2.exo"))); - - assertCacheSpan(new File("parent"), - "A newline (line feed) character \n" - + "A carriage-return character followed immediately by a newline character \r\n" - + "A standalone carriage-return character \r" - + "A next-line character \u0085" - + "A line-separator character \u2028" - + "A paragraph-separator character \u2029", 1, 2); - } - - public void testCacheFileNameRandomData() throws Exception { - Random random = new Random(0); - File parent = new File("parent"); - for (int i = 0; i < 1000; i++) { - String key = TestUtil.buildTestString(1000, random); - long offset = Math.abs(random.nextLong()); - long lastAccessTimestamp = Math.abs(random.nextLong()); - assertCacheSpan(parent, key, offset, lastAccessTimestamp); - } - } - - private void assertCacheSpan(File parent, String key, long offset, long lastAccessTimestamp) { - File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp); - CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile); - String message = cacheFile.toString(); - assertNotNull(message, cacheSpan); - assertEquals(message, parent, cacheFile.getParentFile()); - assertEquals(message, key, cacheSpan.key); - assertEquals(message, offset, cacheSpan.position); - assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp); - } - - private void assertNullCacheSpan(File parent, String key, long offset, - long lastAccessTimestamp) { - File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp); - CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile); - assertNull(cacheFile.toString(), cacheSpan); - } - -} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java new file mode 100644 index 0000000000..4666c81dfb --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -0,0 +1,171 @@ +package com.google.android.exoplayer2.upstream.cache; + +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import android.util.SparseArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +/** + * Tests {@link CachedContentIndex}. + */ +public class CachedContentIndexTest extends InstrumentationTestCase { + + private final byte[] testIndexV1File = { + 0, 0, 0, 1, // version + 0, 0, 0, 0, // flags + 0, 0, 0, 2, // number_of_CachedContent + 0, 0, 0, 5, // cache_id + 0, 5, 65, 66, 67, 68, 69, // cache_key + 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length + 0, 0, 0, 2, // cache_id + 0, 5, 75, 76, 77, 78, 79, // cache_key + 0, 0, 0, 0, 0, 0, 10, 00, // original_content_length + (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array + }; + private CachedContentIndex index; + private File cacheDir; + + @Override + public void setUp() throws Exception { + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + index = new CachedContentIndex(cacheDir); + } + + public void testAddGetRemove() throws Exception { + final String key1 = "key1"; + final String key2 = "key2"; + final String key3 = "key3"; + + // Add two CachedContents with add methods + CachedContent cachedContent1 = new CachedContent(5, key1, 10); + index.addNew(cachedContent1); + CachedContent cachedContent2 = index.add(key2); + assertTrue(cachedContent1.id != cachedContent2.id); + + // add a span + File cacheSpanFile = SimpleCacheSpanTest + .createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30); + SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index); + assertNotNull(span); + cachedContent1.addSpan(span); + + // Check if they are added and get method returns null if the key isn't found + assertEquals(cachedContent1, index.get(key1)); + assertEquals(cachedContent2, index.get(key2)); + assertNull(index.get(key3)); + + // test getAll() + Collection cachedContents = index.getAll(); + assertEquals(2, cachedContents.size()); + assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents)); + + // test getKeys() + Set keys = index.getKeys(); + assertEquals(2, keys.size()); + assertTrue(Arrays.asList(key1, key2).containsAll(keys)); + + // test getKeyForId() + assertEquals(key1, index.getKeyForId(cachedContent1.id)); + assertEquals(key2, index.getKeyForId(cachedContent2.id)); + + // test remove() + index.removeEmpty(key2); + index.removeEmpty(key3); + assertEquals(cachedContent1, index.get(key1)); + assertNull(index.get(key2)); + assertTrue(cacheSpanFile.exists()); + + // test removeEmpty() + index.addNew(cachedContent2); + index.removeEmpty(); + assertEquals(cachedContent1, index.get(key1)); + assertNull(index.get(key2)); + assertTrue(cacheSpanFile.exists()); + } + + public void testStoreAndLoad() throws Exception { + index.addNew(new CachedContent(5, "key1", 10)); + index.add("key2"); + + index.store(); + + CachedContentIndex index2 = new CachedContentIndex(cacheDir); + index2.load(); + + Set keys = index.getKeys(); + Set keys2 = index2.getKeys(); + assertEquals(keys, keys2); + for (String key : keys) { + assertEquals(index.getContentLength(key), index2.getContentLength(key)); + assertEquals(index.get(key).getSpans(), index2.get(key).getSpans()); + } + } + + public void testLoadV1() throws Exception { + FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + fos.write(testIndexV1File); + fos.close(); + + index.load(); + assertEquals(2, index.getAll().size()); + assertEquals(5, index.assignIdForKey("ABCDE")); + assertEquals(10, index.getContentLength("ABCDE")); + assertEquals(2, index.assignIdForKey("KLMNO")); + assertEquals(2560, index.getContentLength("KLMNO")); + } + + public void testStoreV1() throws Exception { + index.addNew(new CachedContent(2, "KLMNO", 2560)); + index.addNew(new CachedContent(5, "ABCDE", 10)); + + index.store(); + + byte[] buffer = new byte[testIndexV1File.length]; + FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + assertEquals(testIndexV1File.length, fos.read(buffer)); + assertEquals(-1, fos.read()); + fos.close(); + + // TODO: The order of the CachedContent stored in index file isn't defined so this test may fail + // on a different implementation of the underlying set + MoreAsserts.assertEquals(testIndexV1File, buffer); + } + + public void testAssignIdForKeyAndGetKeyForId() throws Exception { + final String key1 = "key1"; + final String key2 = "key2"; + int id1 = index.assignIdForKey(key1); + int id2 = index.assignIdForKey(key2); + assertEquals(key1, index.getKeyForId(id1)); + assertEquals(key2, index.getKeyForId(id2)); + assertTrue(id1 != id2); + assertEquals(id1, index.assignIdForKey(key1)); + assertEquals(id2, index.assignIdForKey(key2)); + } + + public void testSetGetContentLength() throws Exception { + final String key1 = "key1"; + assertEquals(C.LENGTH_UNSET, index.getContentLength(key1)); + index.setContentLength(key1, 10); + assertEquals(10, index.getContentLength(key1)); + } + + public void testGetNewId() throws Exception { + SparseArray idToKey = new SparseArray<>(); + assertEquals(0, CachedContentIndex.getNewId(idToKey)); + idToKey.put(10, ""); + assertEquals(11, CachedContentIndex.getNewId(idToKey)); + idToKey.put(Integer.MAX_VALUE, ""); + assertEquals(0, CachedContentIndex.getNewId(idToKey)); + idToKey.put(0, ""); + assertEquals(1, CachedContentIndex.getNewId(idToKey)); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java new file mode 100644 index 0000000000..6ccfc9dee9 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeSet; + +/** + * Unit tests for {@link SimpleCacheSpan}. + */ +public class SimpleCacheSpanTest extends InstrumentationTestCase { + + private CachedContentIndex index; + private File cacheDir; + + public static File createCacheSpanFile(File cacheDir, int id, long offset, int length, + long lastAccessTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + createTestFile(cacheFile, length); + return cacheFile; + } + + public static CacheSpan createCacheSpan(CachedContentIndex index, File cacheDir, String key, + long offset, int length, long lastAccessTimestamp) throws IOException { + int id = index.assignIdForKey(key); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, length, lastAccessTimestamp); + return SimpleCacheSpan.createCacheEntry(cacheFile, index); + } + + @Override + protected void setUp() throws Exception { + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + index = new CachedContentIndex(cacheDir); + } + + @Override + protected void tearDown() throws Exception { + TestUtil.recursiveDelete(cacheDir); + } + + public void testCacheFile() throws Exception { + assertCacheSpan("key1", 0, 0); + assertCacheSpan("key2", 1, 2); + assertCacheSpan("<>:\"/\\|?*%", 1, 2); + assertCacheSpan("key3", 1, 2); + + assertNullCacheSpan(new File("parent"), "key4", -1, 2); + assertNullCacheSpan(new File("parent"), "key5", 1, -2); + + assertCacheSpan( + "A newline (line feed) character \n" + + "A carriage-return character followed immediately by a newline character \r\n" + + "A standalone carriage-return character \r" + + "A next-line character \u0085" + + "A line-separator character \u2028" + + "A paragraph-separator character \u2029", 1, 2); + } + + public void testUpgradeFileName() throws Exception { + String key = "asd\u00aa"; + int id = index.assignIdForKey(key); + File v3file = createTestFile(id + ".0.1.v3.exo"); + File v2file = createTestFile("asd%aa.1.2.v2.exo"); + File wrongEscapedV2file = createTestFile("asd%za.3.4.v2.exo"); + File v1File = createTestFile("asd\u00aa.5.6.v1.exo"); + + SimpleCacheSpan.upgradeOldFiles(cacheDir, index); + + assertTrue(v3file.exists()); + assertFalse(v2file.exists()); + assertTrue(wrongEscapedV2file.exists()); + assertFalse(v1File.exists()); + + File[] files = cacheDir.listFiles(); + assertEquals(4, files.length); + + Set keys = index.getKeys(); + assertEquals("There should be only one key for all files.", 1, keys.size()); + assertTrue(keys.contains(key)); + + TreeSet spans = index.get(key).getSpans(); + assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty()); + + HashMap cachedPositions = new HashMap<>(); + for (File file : files) { + SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, index); + if (cacheSpan != null) { + assertEquals(key, cacheSpan.key); + cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp); + } + } + + assertEquals(1, (long) cachedPositions.get((long) 0)); + assertEquals(2, (long) cachedPositions.get((long) 1)); + assertEquals(6, (long) cachedPositions.get((long) 5)); + } + + private static void createTestFile(File file, int length) throws IOException { + FileOutputStream output = new FileOutputStream(file); + for (int i = 0; i < length; i++) { + output.write(i); + } + output.close(); + } + + private File createTestFile(String name) throws IOException { + File file = new File(cacheDir, name); + createTestFile(file, 1); + return file; + } + + private void assertCacheSpan(String key, long offset, long lastAccessTimestamp) + throws IOException { + int id = index.assignIdForKey(key); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, 1, lastAccessTimestamp); + SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index); + String message = cacheFile.toString(); + assertNotNull(message, cacheSpan); + assertEquals(message, cacheDir, cacheFile.getParentFile()); + assertEquals(message, key, cacheSpan.key); + assertEquals(message, offset, cacheSpan.position); + assertEquals(message, 1, cacheSpan.length); + assertTrue(message, cacheSpan.isCached); + assertEquals(message, cacheFile, cacheSpan.file); + assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp); + } + + private void assertNullCacheSpan(File parent, String key, long offset, + long lastAccessTimestamp) { + File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, + lastAccessTimestamp); + CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index); + assertNull(cacheFile.toString(), cacheSpan); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 2c8ea912fb..5f539c6213 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.File; @@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - // Create a temporary folder - cacheDir = File.createTempFile("SimpleCacheTest", null); - assertTrue(cacheDir.delete()); - assertTrue(cacheDir.mkdir()); + this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); } @Override @@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { } public void testCommittingOneFile() throws Exception { - SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); assertFalse(cacheSpan.isCached); @@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase { } public void testSetGetLength() throws Exception { - SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + SimpleCache simpleCache = getSimpleCache(); assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); - assertTrue(simpleCache.setContentLength(KEY_1, 15)); + simpleCache.setContentLength(KEY_1, 15); assertEquals(15, simpleCache.getContentLength(KEY_1)); simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, 0, 15); - assertTrue(simpleCache.setContentLength(KEY_1, 150)); + simpleCache.setContentLength(KEY_1, 150); assertEquals(150, simpleCache.getContentLength(KEY_1)); addCache(simpleCache, 140, 10); - // Try to set length shorter then the content - assertFalse(simpleCache.setContentLength(KEY_1, 15)); - assertEquals("Content length should be unchanged.", - 150, simpleCache.getContentLength(KEY_1)); - - /* TODO Enable when the length persistance is fixed // Check if values are kept after cache is reloaded. - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); - assertEquals(150, simpleCache.getContentLength(KEY_1)); - CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145); + SimpleCache simpleCache2 = getSimpleCache(); + Set keys = simpleCache.getKeys(); + Set keys2 = simpleCache2.getKeys(); + assertEquals(keys, keys2); + for (String key : keys) { + assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key)); + assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key)); + } // Removing the last span shouldn't cause the length be change next time cache loaded - simpleCache.removeSpan(lastSpan); - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); - assertEquals(150, simpleCache.getContentLength(KEY_1)); - */ + SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); + simpleCache2.removeSpan(lastSpan); + simpleCache2 = getSimpleCache(); + assertEquals(150, simpleCache2.getContentLength(KEY_1)); + } + + private SimpleCache getSimpleCache() { + return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } private void addCache(SimpleCache simpleCache, int position, int length) throws IOException { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 8d74379093..e3d681d6dd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; import java.text.ParseException; import java.util.ArrayList; @@ -146,20 +145,6 @@ public class UtilTest extends TestCase { assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); } - public void testGetHexStringByteArray() throws Exception { - assertHexStringByteArray("", new byte[] {}); - assertHexStringByteArray("01", new byte[] {1}); - assertHexStringByteArray("FF", new byte[] {(byte) 255}); - assertHexStringByteArray("01020304", new byte[] {1, 2, 3, 4}); - assertHexStringByteArray("0123456789ABCDEF", - new byte[] {1, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF}); - } - - private void assertHexStringByteArray(String hex, byte[] array) { - assertEquals(hex, Util.getHexString(array)); - MoreAsserts.assertEquals(array, Util.getBytesFromHexString(hex)); - } - public void testUnescapeInvalidFileName() { assertNull(Util.unescapeFileName("%a")); assertNull(Util.unescapeFileName("%xyz")); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index a8a8de4361..27b989c36f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -187,10 +187,8 @@ public interface Cache { * * @param key The cache key for the data. * @param length The length of the data. - * @return Whether the length was set successfully. Returns false if the length conflicts with the - * existing contents of the cache. */ - boolean setContentLength(String key, long length); + void setContentLength(String key, long length); /** * Returns the content length for the given key if one set, or {@link diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 1f56d4ef83..d53a5d8fe8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -26,7 +25,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; @@ -81,8 +79,6 @@ public final class CacheDataSource implements DataSource { } - private static final String TAG = "CacheDataSource"; - private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; @@ -164,7 +160,7 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key != null ? dataSpec.key : Util.sha1(uri.toString()); + key = dataSpec.key != null ? dataSpec.key : uri.toString(); readPosition = dataSpec.position; currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError; if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { @@ -333,10 +329,7 @@ public final class CacheDataSource implements DataSource { } private void setContentLength(long length) { - if (!cache.setContentLength(key, length)) { - Log.e(TAG, "cache.setContentLength(" + length + ") failed. cache.getContentLength() = " - + cache.getContentLength(key)); - } + cache.setContentLength(key, length); } private void closeCurrentSource() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index d706f4f006..fb96c0fb0e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -16,21 +16,12 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Util; import java.io.File; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}). */ -public final class CacheSpan implements Comparable { - - private static final String SUFFIX = ".v2.exo"; - private static final Pattern CACHE_FILE_PATTERN_V1 = - Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); - private static final Pattern CACHE_FILE_PATTERN_V2 = - Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL); +public class CacheSpan implements Comparable { /** * The cache key that uniquely identifies the original stream. @@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable { */ public final long lastAccessTimestamp; - public static File getCacheFileName(File cacheDir, String key, long offset, - long lastAccessTimestamp) { - return new File(cacheDir, - Util.escapeFileName(key) + "." + offset + "." + lastAccessTimestamp + SUFFIX); - } - - public static CacheSpan createLookup(String key, long position) { - return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null); - } - - public static CacheSpan createOpenHole(String key, long position) { - return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null); - } - - public static CacheSpan createClosedHole(String key, long position, long length) { - return new CacheSpan(key, position, length, false, C.TIME_UNSET, null); + /** + * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated. + * + * @param key The cache key that uniquely identifies the original stream. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + */ + public CacheSpan(String key, long position, long length) { + this(key, position, length, C.TIME_UNSET, null); } /** - * Creates a cache span from an underlying cache file. + * Creates a CacheSpan. * - * @param file The cache file. - * @return The span, or null if the file name is not correctly formatted. + * @param key The cache key that uniquely identifies the original stream. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if + * {@link #isCached} is false. + * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ - public static CacheSpan createCacheEntry(File file) { - Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(file.getName()); - if (!matcher.matches()) { - return null; - } - String key = Util.unescapeFileName(matcher.group(1)); - return key == null ? null : createCacheEntry( - key, Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)), file); - } - - static File upgradeIfNeeded(File file) { - Matcher matcher = CACHE_FILE_PATTERN_V1.matcher(file.getName()); - if (!matcher.matches()) { - return file; - } - String key = matcher.group(1); // Keys were not escaped in version 1. - File newCacheFile = getCacheFileName(file.getParentFile(), key, - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); - file.renameTo(newCacheFile); - return newCacheFile; - } - - private static CacheSpan createCacheEntry(String key, long position, long lastAccessTimestamp, - File file) { - return new CacheSpan(key, position, file.length(), true, lastAccessTimestamp, file); - } - - // Visible for testing. - CacheSpan(String key, long position, long length, boolean isCached, - long lastAccessTimestamp, File file) { + public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) { this.key = key; this.position = position; this.length = length; - this.isCached = isCached; + this.isCached = file != null; this.file = file; this.lastAccessTimestamp = lastAccessTimestamp; } @@ -127,15 +88,10 @@ public final class CacheSpan implements Comparable { } /** - * Renames the file underlying this cache span to update its last access time. - * - * @return A {@link CacheSpan} representing the updated cache file. + * Returns whether this is a hole {@link CacheSpan}. */ - public CacheSpan touch() { - long now = System.currentTimeMillis(); - File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now); - file.renameTo(newCacheFile); - return createCacheEntry(key, position, now, newCacheFile); + public boolean isHoleSpan() { + return !isCached; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java new file mode 100644 index 0000000000..a25688f9db --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.TreeSet; + +/** + * Defines the cached content for a single stream. + */ +/*package*/ final class CachedContent { + + /** + * The cache file id that uniquely identifies the original stream. + */ + public final int id; + /** + * The cache key that uniquely identifies the original stream. + */ + public final String key; + /** + * The cached spans of this content. + */ + private final TreeSet cachedSpans; + /** + * The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown. + */ + private long length; + + /** + * Reads an instance from a {@link DataInputStream}. + * + * @param input Input stream containing values needed to initialize CachedContent instance. + * @throws IOException If an error occurs during reading values. + */ + public CachedContent(DataInputStream input) throws IOException { + this(input.readInt(), input.readUTF(), input.readLong()); + } + + /** + * Creates a CachedContent. + * + * @param id The cache file id. + * @param key The cache stream key. + * @param length The length of the original stream. + */ + public CachedContent(int id, String key, long length) { + this.id = id; + this.key = key; + this.length = length; + this.cachedSpans = new TreeSet<>(); + } + + /** + * Writes the instance to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + public void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(id); + output.writeUTF(key); + output.writeLong(length); + } + + /** Returns the length of the content. */ + public long getLength() { + return length; + } + + /** Sets the length of the content. */ + public void setLength(long length) { + this.length = length; + } + + /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */ + public void addSpan(SimpleCacheSpan span) { + cachedSpans.add(span); + } + + /** Returns a set of all {@link SimpleCacheSpan}s. */ + public TreeSet getSpans() { + return cachedSpans; + } + + /** + * Returns the span containing the position. If there isn't one, it returns a hole span + * which defines the maximum extents of the hole in the cache. + */ + public SimpleCacheSpan getSpan(long position) { + SimpleCacheSpan span = getSpanInternal(position); + if (!span.isCached) { + SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span); + return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position) + : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position); + } + return span; + } + + /** Queries if a range is entirely available in the cache. */ + public boolean isCached(long position, long length) { + SimpleCacheSpan floorSpan = getSpanInternal(position); + if (!floorSpan.isCached) { + // We don't have a span covering the start of the queried region. + return false; + } + long queryEndPosition = position + length; + long currentEndPosition = floorSpan.position + floorSpan.length; + if (currentEndPosition >= queryEndPosition) { + // floorSpan covers the queried region. + return true; + } + for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) { + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + return false; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + return true; + } + } + // We ran out of spans before covering the queried region. + return false; + } + + /** + * Copies the given span with an updated last access time. Passed span becomes invalid after this + * call. + * + * @param cacheSpan Span to be copied and updated. + * @return a span with the updated last access time. + */ + public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) { + // Remove the old span from the in-memory representation. + Assertions.checkState(cachedSpans.remove(cacheSpan)); + // Obtain a new span with updated last access timestamp. + SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); + // Rename the cache file + cacheSpan.file.renameTo(newCacheSpan.file); + // Add the updated span back into the in-memory representation. + cachedSpans.add(newCacheSpan); + return newCacheSpan; + } + + /** Returns whether there are any spans cached. */ + public boolean isEmpty() { + return cachedSpans.isEmpty(); + } + + /** Removes the given span from cache. */ + public boolean removeSpan(CacheSpan span) { + if (cachedSpans.remove(span)) { + span.file.delete(); + return true; + } + return false; + } + + /** Calculates a hash code for the header of this {@code CachedContent}. */ + public int headerHashCode() { + int result = id; + result = 31 * result + key.hashCode(); + result = 31 * result + (int) (length ^ (length >>> 32)); + return result; + } + + /** + * Returns the span containing the position. If there isn't one, it returns the lookup span it + * used for searching. + */ + private SimpleCacheSpan getSpanInternal(long position) { + SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); + SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); + return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan + : floorSpan; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java new file mode 100644 index 0000000000..4f884606ee --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import android.util.SparseArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Set; + +/** + * This class maintains the index of cached content. + */ +/*package*/ final class CachedContentIndex { + + public static final String FILE_NAME = "cached_content_index.exi"; + private static final int VERSION = 1; + + private final HashMap keyToContent; + private final SparseArray idToKey; + private final AtomicFile atomicFile; + private boolean changed; + + /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ + public CachedContentIndex(File cacheDir) { + keyToContent = new HashMap<>(); + idToKey = new SparseArray<>(); + atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); + } + + /** Loads the index file. */ + public void load() { + Assertions.checkState(!changed); + File cacheIndex = atomicFile.getBaseFile(); + if (cacheIndex.exists()) { + if (!readFile()) { + cacheIndex.delete(); + keyToContent.clear(); + idToKey.clear(); + } + } + } + + /** Stores the index data to index file if there is a change. */ + public void store() { + if (!changed) { + return; + } + writeFile(); + changed = false; + } + + /** + * Adds the given key to the index if it isn't there already. + * + * @param key The cache key that uniquely identifies the original stream. + * @return A new or existing CachedContent instance with the given key. + */ + public CachedContent add(String key) { + CachedContent cachedContent = keyToContent.get(key); + if (cachedContent == null) { + cachedContent = addNew(key, C.LENGTH_UNSET); + } + return cachedContent; + } + + /** Returns a CachedContent instance with the given key or null if there isn't one. */ + public CachedContent get(String key) { + return keyToContent.get(key); + } + + /** + * Returns a Collection of all CachedContent instances in the index. The collection is backed by + * the {@code keyToContent} map, so changes to the map are reflected in the collection, and + * vice-versa. If the map is modified while an iteration over the collection is in progress + * (except through the iterator's own remove operation), the results of the iteration are + * undefined. + */ + public Collection getAll() { + return keyToContent.values(); + } + + /** Returns an existing or new id assigned to the given key. */ + public int assignIdForKey(String key) { + return add(key).id; + } + + /** Returns the key which has the given id assigned. */ + public String getKeyForId(int id) { + return idToKey.get(id); + } + + /** + * Removes {@link CachedContent} with the given key from index. It shouldn't contain any spans. + * + * @throws IllegalStateException If {@link CachedContent} isn't empty. + */ + public void removeEmpty(String key) { + CachedContent cachedContent = keyToContent.remove(key); + if (cachedContent != null) { + Assertions.checkState(cachedContent.isEmpty()); + idToKey.remove(cachedContent.id); + changed = true; + } + } + + /** Removes empty {@link CachedContent} instances from index. */ + public void removeEmpty() { + LinkedList cachedContentToBeRemoved = new LinkedList<>(); + for (CachedContent cachedContent : keyToContent.values()) { + if (cachedContent.isEmpty()) { + cachedContentToBeRemoved.add(cachedContent.key); + } + } + for (String key : cachedContentToBeRemoved) { + removeEmpty(key); + } + } + + /** + * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so + * changes to the map are reflected in the set, and vice-versa. If the map is modified while an + * iteration over the set is in progress (except through the iterator's own remove operation), the + * results of the iteration are undefined. + */ + public Set getKeys() { + return keyToContent.keySet(); + } + + /** + * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't + * one already with the given key. + */ + public void setContentLength(String key, long length) { + CachedContent cachedContent = get(key); + if (cachedContent != null) { + if (cachedContent.getLength() != length) { + cachedContent.setLength(length); + changed = true; + } + } else { + addNew(key, length); + } + } + + /** + * Returns the content length for the given key if one set, or {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. + */ + public long getContentLength(String key) { + CachedContent cachedContent = get(key); + return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength(); + } + + private boolean readFile() { + DataInputStream input = null; + try { + input = new DataInputStream(atomicFile.openRead()); + int version = input.readInt(); + if (version != VERSION) { + // Currently there is no other version + return false; + } + input.readInt(); // ignore flags placeholder + int count = input.readInt(); + int hashCode = 0; + for (int i = 0; i < count; i++) { + CachedContent cachedContent = new CachedContent(input); + addNew(cachedContent); + hashCode += cachedContent.headerHashCode(); + } + if (input.readInt() != hashCode) { + return false; + } + } catch (IOException e) { + return false; + } finally { + if (input != null) { + Util.closeQuietly(input); + } + } + return true; + } + + private void writeFile() { + FileOutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + DataOutputStream output = new DataOutputStream(outputStream); + + output.writeInt(VERSION); + output.writeInt(0); // flags placeholder + output.writeInt(keyToContent.size()); + int hashCode = 0; + for (CachedContent cachedContent : keyToContent.values()) { + cachedContent.writeToStream(output); + hashCode += cachedContent.headerHashCode(); + } + output.writeInt(hashCode); + + output.flush(); + atomicFile.finishWrite(outputStream); + } catch (IOException e) { + atomicFile.failWrite(outputStream); + throw new RuntimeException("Writing the new cache index file failed.", e); + } + } + + /** Adds the given CachedContent to the index. */ + /*package*/ void addNew(CachedContent cachedContent) { + keyToContent.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + changed = true; + } + + private CachedContent addNew(String key, long length) { + int id = getNewId(idToKey); + CachedContent cachedContent = new CachedContent(id, key, length); + addNew(cachedContent); + return cachedContent; + } + + /** + * Returns an id which isn't used in the given array. If the maximum id in the array is smaller + * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it + * returns the smallest unused non-negative integer. + */ + //@VisibleForTesting + public static int getNewId(SparseArray idToKey) { + int size = idToKey.size(); + int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); + if (id < 0) { // In case if we pass max int value. + // TODO optimization: defragmentation or binary search? + for (id = 0; id < size; id++) { + if (id != idToKey.keyAt(id)) { + break; + } + } + } + return id; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index a2f2596ad5..f21929a748 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,16 +16,13 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; - -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; +import java.util.LinkedList; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; @@ -38,7 +35,7 @@ public final class SimpleCache implements Cache { private final File cacheDir; private final CacheEvictor evictor; private final HashMap lockedSpans; - private final HashMap>> cachedSpans; + private final CachedContentIndex index; private final HashMap> listeners; private long totalSpace = 0; @@ -52,7 +49,7 @@ public final class SimpleCache implements Cache { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.cachedSpans = new HashMap<>(); + this.index = new CachedContentIndex(cacheDir); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -62,6 +59,7 @@ public final class SimpleCache implements Cache { synchronized (SimpleCache.this) { conditionVariable.open(); initialize(); + SimpleCache.this.evictor.onCacheInitialized(); } } }.start(); @@ -92,13 +90,13 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { - TreeSet spansForKey = getSpansForKey(key); - return spansForKey == null ? null : new TreeSet<>(spansForKey); + CachedContent cachedContent = index.get(key); + return cachedContent == null ? null : new TreeSet(cachedContent.getSpans()); } @Override public synchronized Set getKeys() { - return new HashSet<>(cachedSpans.keySet()); + return new HashSet<>(index.getKeys()); } @Override @@ -107,11 +105,10 @@ public final class SimpleCache implements Cache { } @Override - public synchronized CacheSpan startReadWrite(String key, long position) + public synchronized SimpleCacheSpan startReadWrite(String key, long position) throws InterruptedException { - CacheSpan lookupSpan = CacheSpan.createLookup(key, position); while (true) { - CacheSpan span = startReadWriteNonBlocking(lookupSpan); + SimpleCacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { return span; } else { @@ -125,25 +122,20 @@ public final class SimpleCache implements Cache { } @Override - public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) { - return startReadWriteNonBlocking(CacheSpan.createLookup(key, position)); - } - - private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) { - CacheSpan cacheSpan = getSpan(lookupSpan); + public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) { + SimpleCacheSpan cacheSpan = getSpan(key, position); // Read case. if (cacheSpan.isCached) { // Obtain a new span with updated last access timestamp. - CacheSpan newCacheSpan = cacheSpan.touch(); - replaceSpan(cacheSpan, newCacheSpan); + SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan); notifySpanTouched(cacheSpan, newCacheSpan); return newCacheSpan; } // Write case, lock available. - if (!lockedSpans.containsKey(lookupSpan.key)) { - lockedSpans.put(lookupSpan.key, cacheSpan); + if (!lockedSpans.containsKey(key)) { + lockedSpans.put(key, cacheSpan); return cacheSpan; } @@ -156,16 +148,17 @@ public final class SimpleCache implements Cache { Assertions.checkState(lockedSpans.containsKey(key)); if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. - removeStaleSpans(); + removeStaleSpansAndCachedContents(); cacheDir.mkdirs(); } evictor.onStartFile(this, key, position, maxLength); - return CacheSpan.getCacheFileName(cacheDir, key, position, System.currentTimeMillis()); + return SimpleCacheSpan.getCacheFile(cacheDir, index.assignIdForKey(key), position, + System.currentTimeMillis()); } @Override public synchronized void commitFile(File file) { - CacheSpan span = CacheSpan.createCacheEntry(file); + SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); Assertions.checkState(span != null); Assertions.checkState(lockedSpans.containsKey(span.key)); // If the file doesn't exist, don't add it to the in-memory representation. @@ -183,6 +176,7 @@ public final class SimpleCache implements Cache { Assertions.checkState((span.position + span.length) <= length); } addSpan(span); + index.store(); notifyAll(); } @@ -193,40 +187,33 @@ public final class SimpleCache implements Cache { } /** - * Returns the cache {@link CacheSpan} corresponding to the provided lookup {@link CacheSpan}. - *

- * If the lookup position is contained by an existing entry in the cache, then the returned - * {@link CacheSpan} defines the file in which the data is stored. If the lookup position is not - * contained by an existing entry, then the returned {@link CacheSpan} defines the maximum extents - * of the hole in the cache. + * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link + * SimpleCacheSpan}. * - * @param lookupSpan A lookup {@link CacheSpan} specifying a key and position. - * @return The corresponding cache {@link CacheSpan}. + *

If the lookup position is contained by an existing entry in the cache, then the returned + * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is + * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the + * maximum extents of the hole in the cache. + * + * @param key The key of the span being requested. + * @param position The position of the span being requested. + * @return The corresponding cache {@link SimpleCacheSpan}. */ - private CacheSpan getSpan(CacheSpan lookupSpan) { - String key = lookupSpan.key; - long offset = lookupSpan.position; - TreeSet entries = getSpansForKey(key); - if (entries == null) { - return CacheSpan.createOpenHole(key, lookupSpan.position); + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = index.get(key); + if (cachedContent == null) { + return SimpleCacheSpan.createOpenHole(key, position); } - CacheSpan floorSpan = entries.floor(lookupSpan); - if (floorSpan != null && - floorSpan.position <= offset && offset < floorSpan.position + floorSpan.length) { - // The lookup position is contained within floorSpan. - if (floorSpan.file.exists()) { - return floorSpan; - } else { + while (true) { + SimpleCacheSpan span = cachedContent.getSpan(position); + if (span.isCached && !span.file.exists()) { // The file has been deleted from under us. It's likely that other files will have been // deleted too, so scan the whole in-memory representation. - removeStaleSpans(); - return getSpan(lookupSpan); + removeStaleSpansAndCachedContents(); + continue; } + return span; } - CacheSpan ceilEntry = entries.ceiling(lookupSpan); - return ceilEntry == null ? CacheSpan.createOpenHole(key, lookupSpan.position) : - CacheSpan.createClosedHole(key, lookupSpan.position, - ceilEntry.position - lookupSpan.position); } /** @@ -235,25 +222,37 @@ public final class SimpleCache implements Cache { private void initialize() { if (!cacheDir.exists()) { cacheDir.mkdirs(); + return; } + + index.load(); + + SimpleCacheSpan.upgradeOldFiles(cacheDir, index); + File[] files = cacheDir.listFiles(); if (files == null) { return; } for (File file : files) { - if (file.length() == 0) { - file.delete(); - } else { - file = CacheSpan.upgradeIfNeeded(file); - CacheSpan span = CacheSpan.createCacheEntry(file); - if (span == null) { - file.delete(); - } else { - addSpan(span); + String name = file.getName(); + if (!name.endsWith(SimpleCacheSpan.SUFFIX)) { + if (!name.equals(CachedContentIndex.FILE_NAME)) { + file.delete(); // Delete unknown files } + continue; + } + + SimpleCacheSpan span = file.length() > 0 + ? SimpleCacheSpan.createCacheEntry(file, index) : null; + if (span != null) { + addSpan(span); + } else { + file.delete(); } } - evictor.onCacheInitialized(); + + index.removeEmpty(); + index.store(); } /** @@ -261,59 +260,47 @@ public final class SimpleCache implements Cache { * * @param span The span to be added. */ - private void addSpan(CacheSpan span) { - Pair> entryForKey = cachedSpans.get(span.key); - TreeSet spansForKey; - if (entryForKey == null) { - spansForKey = new TreeSet<>(); - setKeyValue(span.key, C.LENGTH_UNSET, spansForKey); - } else { - spansForKey = entryForKey.second; - } - spansForKey.add(span); + private void addSpan(SimpleCacheSpan span) { + index.add(span.key).addSpan(span); totalSpace += span.length; notifySpanAdded(span); } - @Override - public synchronized void removeSpan(CacheSpan span) { - TreeSet spansForKey = getSpansForKey(span.key); + private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) { + CachedContent cachedContent = index.get(span.key); + Assertions.checkState(cachedContent.removeSpan(span)); totalSpace -= span.length; - Assertions.checkState(spansForKey.remove(span)); - span.file.delete(); - if (spansForKey.isEmpty()) { - cachedSpans.remove(span.key); + if (removeEmptyCachedContent && cachedContent.isEmpty()) { + index.removeEmpty(cachedContent.key); + index.store(); } notifySpanRemoved(span); } + @Override + public synchronized void removeSpan(CacheSpan span) { + removeSpan(span, true); + } + /** * Scans all of the cached spans in the in-memory representation, removing any for which files * no longer exist. */ - private void removeStaleSpans() { - Iterator>>> iterator = - cachedSpans.entrySet().iterator(); - while (iterator.hasNext()) { - Entry>> next = iterator.next(); - Iterator spanIterator = next.getValue().second.iterator(); - boolean isEmpty = true; - while (spanIterator.hasNext()) { - CacheSpan span = spanIterator.next(); + private void removeStaleSpansAndCachedContents() { + LinkedList spansToBeRemoved = new LinkedList<>(); + for (CachedContent cachedContent : index.getAll()) { + for (CacheSpan span : cachedContent.getSpans()) { if (!span.file.exists()) { - spanIterator.remove(); - if (span.isCached) { - totalSpace -= span.length; - } - notifySpanRemoved(span); - } else { - isEmpty = false; + spansToBeRemoved.add(span); } } - if (isEmpty) { - iterator.remove(); - } } + for (CacheSpan span : spansToBeRemoved) { + // Remove span but not CachedContent to prevent multiple index.store() calls. + removeSpan(span, false); + } + index.removeEmpty(); + index.store(); } private void notifySpanRemoved(CacheSpan span) { @@ -326,7 +313,7 @@ public final class SimpleCache implements Cache { evictor.onSpanRemoved(this, span); } - private void notifySpanAdded(CacheSpan span) { + private void notifySpanAdded(SimpleCacheSpan span) { ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { @@ -336,7 +323,7 @@ public final class SimpleCache implements Cache { evictor.onSpanAdded(this, span); } - private void notifySpanTouched(CacheSpan oldSpan, CacheSpan newSpan) { + private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) { ArrayList keyListeners = listeners.get(oldSpan.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { @@ -348,82 +335,22 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { - TreeSet entries = getSpansForKey(key); - if (entries == null) { + CachedContent cachedContent = index.get(key); + if (cachedContent == null) { return false; } - CacheSpan lookupSpan = CacheSpan.createLookup(key, position); - CacheSpan floorSpan = entries.floor(lookupSpan); - if (floorSpan == null || floorSpan.position + floorSpan.length <= position) { - // We don't have a span covering the start of the queried region. - return false; - } - long queryEndPosition = position + length; - long currentEndPosition = floorSpan.position + floorSpan.length; - if (currentEndPosition >= queryEndPosition) { - // floorSpan covers the queried region. - return true; - } - for (CacheSpan next : entries.tailSet(floorSpan, false)) { - if (next.position > currentEndPosition) { - // There's a hole in the cache within the queried region. - return false; - } - // We expect currentEndPosition to always equal (next.position + next.length), but - // perform a max check anyway to guard against the existence of overlapping spans. - currentEndPosition = Math.max(currentEndPosition, next.position + next.length); - if (currentEndPosition >= queryEndPosition) { - // We've found spans covering the queried region. - return true; - } - } - // We ran out of spans before covering the queried region. - return false; + return cachedContent.isCached(position, length); } @Override - public synchronized boolean setContentLength(String key, long length) { - Pair> entryForKey = cachedSpans.get(key); - TreeSet entries; - if (entryForKey != null) { - entries = entryForKey.second; - if (entries != null && !entries.isEmpty()) { - CacheSpan last = entries.last(); - long end = last.position + last.length; - if (end > length) { - return false; - } - } - } else { - entries = new TreeSet<>(); - } - // TODO persist the length value - setKeyValue(key, length, entries); - return true; + public synchronized void setContentLength(String key, long length) { + index.setContentLength(key, length); + index.store(); } @Override public synchronized long getContentLength(String key) { - Pair> entryForKey = cachedSpans.get(key); - return entryForKey == null ? C.LENGTH_UNSET : entryForKey.first; - } - - - private TreeSet getSpansForKey(String key) { - Pair> entryForKey = cachedSpans.get(key); - return entryForKey != null ? entryForKey.second : null; - } - - private void setKeyValue(String key, long length, TreeSet entries) { - cachedSpans.put(key, Pair.create(length, entries)); - } - - private void replaceSpan(CacheSpan oldSpan, CacheSpan newSpan) { - // Remove the old span from the in-memory representation. - TreeSet spansForKey = getSpansForKey(oldSpan.key); - Assertions.checkState(spansForKey.remove(oldSpan)); - // Add the updated span back into the in-memory representation. - spansForKey.add(newSpan); + return index.getContentLength(key); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java new file mode 100644 index 0000000000..deac524e2a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class stores span metadata in filename. + */ +/*package*/ final class SimpleCacheSpan extends CacheSpan { + + private static final String FILE_EXTENSION = "exo"; + public static final String SUFFIX = ".v3." + FILE_EXTENSION; + private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( + "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( + "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( + "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + + public static File getCacheFile(File cacheDir, int id, long position, + long lastAccessTimestamp) { + return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); + } + + public static SimpleCacheSpan createLookup(String key, long position) { + return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); + } + + public static SimpleCacheSpan createOpenHole(String key, long position) { + return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); + } + + public static SimpleCacheSpan createClosedHole(String key, long position, long length) { + return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); + } + + /** + * Creates a cache span from an underlying cache file. + * + * @param file The cache file. + * @param index Cached content index. + * @return The span, or null if the file name is not correctly formatted, or if the id is not + * present in the content index. + */ + public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { + Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(file.getName()); + if (!matcher.matches()) { + return null; + } + long length = file.length(); + int id = Integer.parseInt(matcher.group(1)); + String key = index.getKeyForId(id); + return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length, + Long.parseLong(matcher.group(3)), file); + } + + /** Upgrades span files with old versions. */ + public static void upgradeOldFiles(File cacheDir, CachedContentIndex index) { + for (File file : cacheDir.listFiles()) { + String name = file.getName(); + if (!name.endsWith(SUFFIX) && name.endsWith(FILE_EXTENSION)) { + upgradeFile(file, index); + } + } + } + + private static void upgradeFile(File file, CachedContentIndex index) { + String key; + String filename = file.getName(); + Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename); + if (matcher.matches()) { + key = Util.unescapeFileName(matcher.group(1)); + if (key == null) { + return; + } + } else { + matcher = CACHE_FILE_PATTERN_V1.matcher(filename); + if (!matcher.matches()) { + return; + } + key = matcher.group(1); // Keys were not escaped in version 1. + } + + File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + file.renameTo(newCacheFile); + } + + private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, + File file) { + super(key, position, length, lastAccessTimestamp, file); + } + + /** + * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This + * doesn't copy or change the underlying cache file. + * + * @param id The cache file id. + * @return A {@link SimpleCacheSpan} with updated last access time stamp. + * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). + */ + public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) { + Assertions.checkState(isCached); + long now = System.currentTimeMillis(); + File newCacheFile = getCacheFile(file.getParentFile(), id, position, now); + return new SimpleCacheSpan(key, position, length, now, newCacheFile); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java new file mode 100644 index 0000000000..3746a741e0 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2009 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.util; + +import android.util.Log; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Exoplayer internal version of the framework's {@link android.util.AtomicFile}, + * a helper class for performing atomic operations on a file by creating a + * backup file until a write has successfully completed. + *

+ * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + *

+ * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + *

+ */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + /** + * Create a new AtomicFile for a file located at the given File path. + * The secondary backup file will be the same file path with ".bak" appended. + */ + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + /** + * Return the path to the base file. You should not generally use this, + * as the data at that path may not be valid. + */ + public File getBaseFile() { + return mBaseName; + } + + /** + * Delete the atomic file. This deletes both the base and backup files. + */ + public void delete() { + mBaseName.delete(); + mBackupName.delete(); + } + + /** + * Start a new write operation on the file. This returns a FileOutputStream + * to which you can write the new file data. The existing file is replaced + * with the new data. You must not directly close the given + * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} + * or {@link #failWrite(FileOutputStream)}. + * + *

Note that if another thread is currently performing + * a write, this will simply replace whatever that thread is writing + * with the new file being written by this thread, and when the other + * thread finishes the write the new write operation will no longer be + * safe (or will be lost). You must do your own threading protection for + * access to AtomicFile. + */ + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBackupName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } else { + mBaseName.delete(); + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdirs()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + /** + * Call when you have successfully finished writing to the stream + * returned by {@link #startWrite()}. This will close, sync, and + * commit the new data. The next attempt to read the atomic file + * will return the new file stream. + */ + public void finishWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + /** + * Call when you have failed for some reason at writing to the stream + * returned by {@link #startWrite()}. This will close the current + * write stream, and roll back to the previous state of the file. + */ + public void failWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + /** + * Open the atomic file for reading. If there previously was an + * incomplete write, this will roll back to the last good data before + * opening for read. You should call close() on the FileInputStream when + * you are done reading from it. + * + *

Note that if another thread is currently performing + * a write, this will incorrectly consider it to be in the state of a bad + * write and roll back, causing the new data currently being written to + * be dropped. You must do your own threading protection for access to + * AtomicFile. + */ + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + /** + * A convenience for {@link #openRead()} that also reads all of the + * file contents into a byte array which is returned. + */ + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length - pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length - pos) { + byte[] newData = new byte[pos + avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } + + private static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + // do nothing + } + return false; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index c4505fd8b9..0184018bc9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -34,15 +34,12 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; @@ -97,7 +94,6 @@ public final class Util { Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); - private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); private Util() {} @@ -215,13 +211,14 @@ public final class Util { } /** - * Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur. + * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link + * java.io.OutputStream} and {@link InputStream} are {@code Closeable}. * - * @param outputStream The {@link OutputStream} to close. + * @param closeable The {@link Closeable} to close. */ - public static void closeQuietly(OutputStream outputStream) { + public static void closeQuietly(Closeable closeable) { try { - outputStream.close(); + closeable.close(); } catch (IOException e) { // Ignore. } @@ -630,21 +627,6 @@ public final class Util { return data; } - /** - * Returns a hex string representation of the given byte array. - * - * @param bytes The byte array. - */ - public static String getHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - int i = 0; - for (byte v : bytes) { - hexChars[i++] = HEX_DIGITS[(v >> 4) & 0xf]; - hexChars[i++] = HEX_DIGITS[v & 0xf]; - } - return new String(hexChars); - } - /** * Returns a string with comma delimited simple names of each object's class. * @@ -869,22 +851,6 @@ public final class Util { return initialValue; } - /** - * Returns the SHA-1 digest of {@code input} as a hex string. - * - * @param input The string whose SHA-1 digest is required. - */ - public static String sha1(String input) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = input.getBytes("UTF-8"); - digest.update(bytes, 0, bytes.length); - return getHexString(digest.digest()); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - /** * Gets the physical size of the default display, in pixels. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 6f4578b694..5962bf9911 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; +import android.content.Context; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -313,4 +314,12 @@ public class TestUtil { fileOrDirectory.delete(); } + /** Creates an empty folder in the application specific cache directory. */ + public static File createTempFolder(Context context) throws IOException { + File tempFolder = File.createTempFile("ExoPlayerTest", null, context.getCacheDir()); + Assert.assertTrue(tempFolder.delete()); + Assert.assertTrue(tempFolder.mkdir()); + return tempFolder; + } + } From a6e277011617ff0b466925d08a4f98c14143d09d Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 7 Nov 2016 05:14:38 -0800 Subject: [PATCH 069/206] Upgrade SimpleCacheSpan files during createCacheEntry call. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138379386 --- .../upstream/cache/CacheDataSourceTest.java | 2 +- .../upstream/cache/SimpleCacheSpanTest.java | 4 +- .../upstream/cache/SimpleCache.java | 9 +--- .../upstream/cache/SimpleCacheSpan.java | 42 +++++++++---------- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index c0d9570d7a..18e39be93c 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -54,7 +54,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { assertReadDataContentLength(cacheDataSource, false, false); File[] files = cacheDir.listFiles(); for (File file : files) { - if (file.getName().endsWith(SimpleCacheSpan.SUFFIX)) { + if (!file.getName().equals(CachedContentIndex.FILE_NAME)) { assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 6ccfc9dee9..a4fbb2af4d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -83,7 +83,9 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { File wrongEscapedV2file = createTestFile("asd%za.3.4.v2.exo"); File v1File = createTestFile("asd\u00aa.5.6.v1.exo"); - SimpleCacheSpan.upgradeOldFiles(cacheDir, index); + for (File file : cacheDir.listFiles()) { + SimpleCacheSpan.createCacheEntry(file, index); + } assertTrue(v3file.exists()); assertFalse(v2file.exists()); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index f21929a748..53a44a5797 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -227,21 +227,14 @@ public final class SimpleCache implements Cache { index.load(); - SimpleCacheSpan.upgradeOldFiles(cacheDir, index); - File[] files = cacheDir.listFiles(); if (files == null) { return; } for (File file : files) { - String name = file.getName(); - if (!name.endsWith(SimpleCacheSpan.SUFFIX)) { - if (!name.equals(CachedContentIndex.FILE_NAME)) { - file.delete(); // Delete unknown files - } + if (file.getName().equals(CachedContentIndex.FILE_NAME)) { continue; } - SimpleCacheSpan span = file.length() > 0 ? SimpleCacheSpan.createCacheEntry(file, index) : null; if (span != null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index deac524e2a..47aefc7820 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -27,14 +27,13 @@ import java.util.regex.Pattern; */ /*package*/ final class SimpleCacheSpan extends CacheSpan { - private static final String FILE_EXTENSION = "exo"; - public static final String SUFFIX = ".v3." + FILE_EXTENSION; + private static final String SUFFIX = ".v3.exo"; private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( - "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( - "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL); private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( - "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL); public static File getCacheFile(File cacheDir, int id, long position, long lastAccessTimestamp) { @@ -54,7 +53,7 @@ import java.util.regex.Pattern; } /** - * Creates a cache span from an underlying cache file. + * Creates a cache span from an underlying cache file. Upgrades the file if necessary. * * @param file The cache file. * @param index Cached content index. @@ -62,7 +61,15 @@ import java.util.regex.Pattern; * present in the content index. */ public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { - Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(file.getName()); + String name = file.getName(); + if (!name.endsWith(SUFFIX)) { + file = upgradeFile(file, index); + if (file == null) { + return null; + } + } + + Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name); if (!matcher.matches()) { return null; } @@ -73,36 +80,29 @@ import java.util.regex.Pattern; Long.parseLong(matcher.group(3)), file); } - /** Upgrades span files with old versions. */ - public static void upgradeOldFiles(File cacheDir, CachedContentIndex index) { - for (File file : cacheDir.listFiles()) { - String name = file.getName(); - if (!name.endsWith(SUFFIX) && name.endsWith(FILE_EXTENSION)) { - upgradeFile(file, index); - } - } - } - - private static void upgradeFile(File file, CachedContentIndex index) { + private static File upgradeFile(File file, CachedContentIndex index) { String key; String filename = file.getName(); Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename); if (matcher.matches()) { key = Util.unescapeFileName(matcher.group(1)); if (key == null) { - return; + return null; } } else { matcher = CACHE_FILE_PATTERN_V1.matcher(filename); if (!matcher.matches()) { - return; + return null; } key = matcher.group(1); // Keys were not escaped in version 1. } File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); - file.renameTo(newCacheFile); + if (!file.renameTo(newCacheFile)) { + return null; + } + return newCacheFile; } private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, From 4cd8c77053bc3bde32133b7c9e196c68bffafc51 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 7 Nov 2016 06:27:48 -0800 Subject: [PATCH 070/206] Add layer of indirection for DRM. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138383979 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 4 +- .../ext/flac/LibflacAudioRenderer.java | 4 +- .../ext/opus/LibopusAudioRenderer.java | 22 ++- .../exoplayer2/ext/opus/OpusDecoder.java | 44 +++++- .../ext/opus/OpusDecoderException.java | 4 + .../exoplayer2/ext/opus/OpusLibrary.java | 2 +- extensions/opus/src/main/jni/opus_jni.cc | 27 +++- .../ext/vp9/LibvpxVideoRenderer.java | 136 +++++++++++++++++- .../exoplayer2/ext/vp9/VpxDecoder.java | 40 +++++- .../ext/vp9/VpxDecoderException.java | 9 +- .../exoplayer2/ext/vp9/VpxLibrary.java | 2 +- extensions/vp9/src/main/jni/vpx_jni.cc | 24 ++++ .../audio/AudioDecoderException.java | 11 ++ .../audio/SimpleDecoderAudioRenderer.java | 132 +++++++++++++++-- .../exoplayer2/drm/DecryptionException.java | 20 +++ 15 files changed, 445 insertions(+), 36 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 838ed1c3e9..75d23dfa2e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -71,7 +72,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected FfmpegDecoder createDecoder(Format format) throws FfmpegDecoderException { + protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + throws FfmpegDecoderException { decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, format.sampleMimeType, format.initializationData); return decoder; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 931b5ff3d9..0562851d3e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -63,7 +64,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected FlacDecoder createDecoder(Format format) throws FlacDecoderException { + protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + throws FlacDecoderException { return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData); } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 3393562104..60e5ff34b4 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -57,6 +59,21 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { super(eventHandler, eventListener, audioCapabilities, streamType); } + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param streamType The type of audio stream for the {@link AudioTrack}. + */ + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioCapabilities audioCapabilities, int streamType, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + super(eventHandler, eventListener, audioCapabilities, streamType, drmSessionManager, + playClearSamplesWithoutKeys); + } + @Override public int supportsFormat(Format format) { return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType) @@ -64,9 +81,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected OpusDecoder createDecoder(Format format) throws OpusDecoderException { + protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + throws OpusDecoderException { return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.initializationData); + format.initializationData, mediaCrypto); } } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 73fb4072e8..6d0deb44ae 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -16,9 +16,12 @@ package com.google.android.exoplayer2.ext.opus; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; +import com.google.android.exoplayer2.drm.DecryptionException; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; @@ -36,6 +39,12 @@ import java.util.List; */ private static final int SAMPLE_RATE = 48000; + private static final int NO_ERROR = 0; + private static final int DECODE_ERROR = -1; + private static final int DRM_ERROR = -2; + + private final ExoMediaCrypto exoMediaCrypto; + private final int channelCount; private final int headerSkipSamples; private final int headerSeekPreRollSamples; @@ -52,14 +61,20 @@ import java.util.List; * @param initializationData Codec-specific initialization data. The first element must contain an * opus header. Optionally, the list may contain two additional buffers, which must contain * the encoder delay and seek pre roll values in nanoseconds, encoded as longs. + * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted + * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder. */ public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - List initializationData) throws OpusDecoderException { + List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!OpusLibrary.isAvailable()) { throw new OpusDecoderException("Failed to load decoder native libraries."); } + this.exoMediaCrypto = exoMediaCrypto; + if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) { + throw new OpusDecoderException("Opus decoder does not support secure decode."); + } byte[] headerBytes = initializationData.get(0); if (headerBytes.length < 19) { throw new OpusDecoderException("Header size is too small."); @@ -139,11 +154,25 @@ import java.util.List; skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; } ByteBuffer inputData = inputBuffer.data; - int result = opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), - outputBuffer, SAMPLE_RATE); + CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; + int result = inputBuffer.isEncrypted() + ? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), + outputBuffer, SAMPLE_RATE, exoMediaCrypto, cryptoInfo.mode, + cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData) + : opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), + outputBuffer, SAMPLE_RATE); if (result < 0) { - return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result)); + if (result == DRM_ERROR) { + String message = "Drm error: " + opusGetErrorMessage(nativeDecoderContext); + DecryptionException cause = new DecryptionException( + opusGetErrorCode(nativeDecoderContext), message); + return new OpusDecoderException(message, cause); + } else { + return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result)); + } } + ByteBuffer outputData = outputBuffer.data; outputData.position(0); outputData.limit(result); @@ -182,8 +211,13 @@ import java.util.List; int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate); + private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, + int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, + ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native void opusClose(long decoder); private native void opusReset(long decoder); - private native String opusGetErrorMessage(int errorCode); + private native int opusGetErrorCode(long decoder); + private native String opusGetErrorMessage(long decoder); } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java index 338f3ea94e..6645086838 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java @@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException { super(message); } + /* package */ OpusDecoderException(String message, Throwable cause) { + super(message, cause); + } + } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index a79ef6df3a..41a28b9fd7 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -50,5 +50,5 @@ public final class OpusLibrary { } public static native String opusGetVersion(); - + public static native boolean opusIsSecureDecodeSupported(); } diff --git a/extensions/opus/src/main/jni/opus_jni.cc b/extensions/opus/src/main/jni/opus_jni.cc index 0920d9e499..48c1bd5e6d 100644 --- a/extensions/opus/src/main/jni/opus_jni.cc +++ b/extensions/opus/src/main/jni/opus_jni.cc @@ -60,11 +60,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples. static int channelCount; +static int errorCode; DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount, jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) { int status = OPUS_INVALID_STATE; ::channelCount = channelCount; + errorCode = 0; jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0); uint8_t* streamMap = reinterpret_cast(streamMapBytes); OpusMSDecoder* decoder = opus_multistream_decoder_create( @@ -109,10 +111,24 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, env->GetDirectBufferAddress(jOutputBufferData)); int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize, outputBufferData, outputSize, 0); + // record error code + errorCode = (sampleCount < 0) ? sampleCount : 0; return (sampleCount < 0) ? sampleCount : sampleCount * kBytesPerSample * channelCount; } +DECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs, + jobject jInputBuffer, jint inputSize, jobject jOutputBuffer, + jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key, + jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData, + jintArray numBytesOfEncryptedData) { + // Doesn't support + // Java client should have checked vpxSupportSecureDecode + // and avoid calling this + // return -2 (DRM Error) + return -2; +} + DECODER_FUNC(void, opusClose, jlong jDecoder) { OpusMSDecoder* decoder = reinterpret_cast(jDecoder); opus_multistream_decoder_destroy(decoder); @@ -123,10 +139,19 @@ DECODER_FUNC(void, opusReset, jlong jDecoder) { opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE); } -DECODER_FUNC(jstring, opusGetErrorMessage, jint errorCode) { +DECODER_FUNC(jstring, opusGetErrorMessage, jlong jContext) { return env->NewStringUTF(opus_strerror(errorCode)); } +DECODER_FUNC(jint, opusGetErrorCode, jlong jContext) { + return errorCode; +} + +LIBRARY_FUNC(jstring, opusIsSecureDecodeSupported) { + // Doesn't support + return 0; +} + LIBRARY_FUNC(jstring, opusGetVersion) { return env->NewStringUTF(opus_get_version_string()); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index cfdd962197..ddda5fe1f8 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.vp9; import android.graphics.Bitmap; import android.graphics.Canvas; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; @@ -28,8 +29,12 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; @@ -56,8 +61,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private final boolean scaleToFit; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; + private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final FormatHolder formatHolder; + private final DrmSessionManager drmSessionManager; private DecoderCounters decoderCounters; private Format format; @@ -65,6 +72,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private DecoderInputBuffer inputBuffer; private VpxOutputBuffer outputBuffer; private VpxOutputBuffer nextOutputBuffer; + private DrmSession drmSession; + private DrmSession pendingDrmSession; private Bitmap bitmap; private boolean renderedFirstFrame; @@ -72,6 +81,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private Surface surface; private VpxOutputBufferRenderer outputBufferRenderer; private int outputMode; + private boolean waitingForKeys; private boolean inputStreamEnded; private boolean outputStreamEnded; @@ -104,10 +114,37 @@ public final class LibvpxVideoRenderer extends BaseRenderer { public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { + this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify, + null, false); + } + + /** + * @param scaleToFit Whether video frames should be scaled to fit when rendering. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + */ + public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, + Handler eventHandler, VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { super(C.TRACK_TYPE_VIDEO); this.scaleToFit = scaleToFit; this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; joiningDeadlineMs = -1; previousWidth = -1; previousHeight = -1; @@ -135,12 +172,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } if (isRendererAvailable()) { + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState == DrmSession.STATE_OPENED + || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { + mediaCrypto = drmSession.getMediaCrypto(); + } else { + // The drm session isn't open yet. + return; + } + } try { if (decoder == null) { // If we don't have a decoder yet, we need to instantiate one. long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE); + decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); @@ -258,7 +310,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { surface.unlockCanvasAndPost(canvas); } - private boolean feedInputBuffer() throws VpxDecoderException { + private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { if (inputStreamEnded) { return false; } @@ -270,7 +322,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - int result = readSource(formatHolder, inputBuffer); + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer); + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -284,6 +343,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer { inputBuffer = null; return false; } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } inputBuffer.flip(); decoder.queueInputBuffer(inputBuffer); decoderCounters.inputBufferCount++; @@ -291,8 +355,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return true; } + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (drmSession == null) { + return false; + } + int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS + && (bufferEncrypted || !playClearSamplesWithoutKeys); + } + private void flushDecoder() { inputBuffer = null; + waitingForKeys = false; if (outputBuffer != null) { outputBuffer.release(); outputBuffer = null; @@ -311,6 +388,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override public boolean isReady() { + if (waitingForKeys) { + return false; + } if (format != null && (isSourceReady() || outputBuffer != null) && (renderedFirstFrame || !isRendererAvailable())) { // Ready. If we were joining then we've now joined, so clear the joining deadline. @@ -365,11 +445,26 @@ public final class LibvpxVideoRenderer extends BaseRenderer { inputBuffer = null; outputBuffer = null; format = null; + waitingForKeys = false; try { releaseDecoder(); } finally { - decoderCounters.ensureUpdated(); - eventDispatcher.disabled(decoderCounters); + try { + if (drmSession != null) { + drmSessionManager.releaseSession(drmSession); + } + } finally { + try { + if (pendingDrmSession != null && pendingDrmSession != drmSession) { + drmSessionManager.releaseSession(pendingDrmSession); + } + } finally { + drmSession = null; + pendingDrmSession = null; + decoderCounters.ensureUpdated(); + eventDispatcher.disabled(decoderCounters); + } + } } } @@ -378,10 +473,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer { decoder.release(); decoder = null; decoderCounters.decoderReleaseCount++; + waitingForKeys = false; + if (drmSession != null && pendingDrmSession != drmSession) { + try { + drmSessionManager.releaseSession(drmSession); + } finally { + drmSession = null; + } + } } } - private boolean readFormat() { + private boolean readFormat() throws ExoPlaybackException { int result = readSource(formatHolder, null); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); @@ -390,8 +493,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } - private void onInputFormatChanged(Format newFormat) { + private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + Format oldFormat = format; format = newFormat; + + boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null + : oldFormat.drmInitData); + if (drmInitDataChanged) { + if (format.drmInitData != null) { + if (drmSessionManager == null) { + throw ExoPlaybackException.createForRenderer( + new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); + } + pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); + if (pendingDrmSession == drmSession) { + drmSessionManager.releaseSession(pendingDrmSession); + } + } else { + pendingDrmSession = null; + } + } + eventDispatcher.inputFormatChanged(format); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 5407e94f42..9af997a58c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -16,8 +16,11 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.drm.DecryptionException; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import java.nio.ByteBuffer; /** @@ -30,6 +33,11 @@ import java.nio.ByteBuffer; public static final int OUTPUT_MODE_YUV = 0; public static final int OUTPUT_MODE_RGB = 1; + private static final int NO_ERROR = 0; + private static final int DECODE_ERROR = 1; + private static final int DRM_ERROR = 2; + + private final ExoMediaCrypto exoMediaCrypto; private final long vpxDecContext; private volatile int outputMode; @@ -40,14 +48,20 @@ import java.nio.ByteBuffer; * @param numInputBuffers The number of input buffers. * @param numOutputBuffers The number of output buffers. * @param initialInputBufferSize The initial size of each input buffer. + * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted + * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ - public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize) - throws VpxDecoderException { + public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, + ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); } + this.exoMediaCrypto = exoMediaCrypto; + if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { + throw new VpxDecoderException("Vpx decoder does not support secure decode."); + } vpxDecContext = vpxInit(); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); @@ -90,9 +104,23 @@ import java.nio.ByteBuffer; boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); - if (vpxDecode(vpxDecContext, inputData, inputSize) != 0) { - return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext)); + CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; + final long result = inputBuffer.isEncrypted() + ? vpxSecureDecode(vpxDecContext, inputData, inputSize, exoMediaCrypto, + cryptoInfo.mode, cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData) + : vpxDecode(vpxDecContext, inputData, inputSize); + if (result != NO_ERROR) { + if (result == DRM_ERROR) { + String message = "Drm error: " + vpxGetErrorMessage(vpxDecContext); + DecryptionException cause = new DecryptionException( + vpxGetErrorCode(vpxDecContext), message); + return new VpxDecoderException(message, cause); + } else { + return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext)); + } } + outputBuffer.init(inputBuffer.timeUs, outputMode); if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); @@ -109,7 +137,11 @@ import java.nio.ByteBuffer; private native long vpxInit(); private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); + private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, + ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); + private native int vpxGetErrorCode(long context); private native String vpxGetErrorMessage(long context); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java index 94ed8e9fdd..5f43b503ac 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java @@ -20,8 +20,11 @@ package com.google.android.exoplayer2.ext.vp9; */ public class VpxDecoderException extends Exception { - /* package */ VpxDecoderException(String message) { - super(message); - } + /* package */ VpxDecoderException(String message) { + super(message); + } + /* package */ VpxDecoderException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 6c694ebd2c..2caa33c17c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -59,5 +59,5 @@ public final class VpxLibrary { private static native String vpxGetVersion(); private static native String vpxGetBuildConfig(); - + public static native boolean vpxIsSecureDecodeSupported(); } diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index a07b30a728..afaac1c8ae 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -59,6 +59,7 @@ static jmethodID initForRgbFrame; static jmethodID initForYuvFrame; static jfieldID dataField; static jfieldID outputModeField; +static int errorCode; jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; @@ -72,6 +73,7 @@ DECODER_FUNC(jlong, vpxInit) { vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); + errorCode = 0; if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { LOGE("ERROR: Fail to initialize libvpx decoder."); return 0; @@ -97,13 +99,26 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { reinterpret_cast(env->GetDirectBufferAddress(encoded)); const vpx_codec_err_t status = vpx_codec_decode(context, buffer, len, NULL, 0); + errorCode = 0; if (status != VPX_CODEC_OK) { LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); + errorCode = status; return -1; } return 0; } +DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len, + jobject mediaCrypto, jint inputMode, jbyteArray&, jbyteArray&, + jint inputNumSubSamples, jintArray numBytesOfClearData, + jintArray numBytesOfEncryptedData) { + // Doesn't support + // Java client should have checked vpxSupportSecureDecode + // and avoid calling this + // return -2 (DRM Error) + return -2; +} + DECODER_FUNC(jlong, vpxClose, jlong jContext) { vpx_codec_ctx_t* const context = reinterpret_cast(jContext); vpx_codec_destroy(context); @@ -181,6 +196,15 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) { return env->NewStringUTF(vpx_codec_error(context)); } +DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { + return errorCode; +} + +LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) { + // Doesn't support + return 0; +} + LIBRARY_FUNC(jstring, vpxGetVersion) { return env->NewStringUTF(vpx_codec_version_str()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java index d0ad44f8da..b5ee052924 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java @@ -27,4 +27,15 @@ public abstract class AudioDecoderException extends Exception { super(detailMessage); } + /** + * @param detailMessage The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public AudioDecoderException(String detailMessage, Throwable cause) { + super(detailMessage, cause); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 6f15945d9e..f6b1dc14ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio; import android.media.AudioManager; import android.media.PlaybackParams; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -29,17 +30,24 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; /** * Decodes and renders audio using a {@link SimpleDecoder}. */ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { + private final boolean playClearSamplesWithoutKeys; + private final EventDispatcher eventDispatcher; private final FormatHolder formatHolder; + private final DrmSessionManager drmSessionManager; private DecoderCounters decoderCounters; private Format inputFormat; @@ -47,11 +55,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ? extends AudioDecoderException> decoder; private DecoderInputBuffer inputBuffer; private SimpleOutputBuffer outputBuffer; + private DrmSession drmSession; + private DrmSession pendingDrmSession; private long currentPositionUs; private boolean allowPositionDiscontinuity; private boolean inputStreamEnded; private boolean outputStreamEnded; + private boolean waitingForKeys; private final AudioTrack audioTrack; private int audioSessionId; @@ -70,7 +81,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - this (eventHandler, eventListener, null, AudioManager.STREAM_MUSIC); + this(eventHandler, eventListener, null, AudioManager.STREAM_MUSIC); } /** @@ -84,7 +95,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, int streamType) { + this(eventHandler, eventListener, audioCapabilities, streamType, null, false); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param streamType The type of audio stream for the {@link AudioTrack}. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + */ + public SimpleDecoderAudioRenderer(Handler eventHandler, + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + int streamType, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { super(C.TRACK_TYPE_AUDIO); + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioTrack = new AudioTrack(audioCapabilities, streamType); @@ -108,12 +143,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return; } + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState == DrmSession.STATE_OPENED + || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { + mediaCrypto = drmSession.getMediaCrypto(); + } else { + // The drm session isn't open yet. + return; + } + } // If we don't have a decoder yet, we need to instantiate one. if (decoder == null) { try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createAudioDecoder"); - decoder = createDecoder(inputFormat); + decoder = createDecoder(inputFormat, mediaCrypto); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, @@ -141,11 +190,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * Creates a decoder for the given format. * * @param format The format for which a decoder is required. + * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. + * Maybe null and can be ignored if decoder does not handle encrypted content. * @return The decoder. * @throws AudioDecoderException If an error occurred creating a suitable decoder. */ protected abstract SimpleDecoder createDecoder(Format format) throws AudioDecoderException; + ? extends AudioDecoderException> createDecoder(Format format, ExoMediaCrypto mediaCrypto) + throws AudioDecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first @@ -228,7 +280,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } - private boolean feedInputBuffer() throws AudioDecoderException { + private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException { if (inputStreamEnded) { return false; } @@ -240,7 +292,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } - int result = readSource(formatHolder, inputBuffer); + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer); + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -254,6 +313,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements inputBuffer = null; return false; } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } inputBuffer.flip(); decoder.queueInputBuffer(inputBuffer); decoderCounters.inputBufferCount++; @@ -261,8 +325,21 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return true; } + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (drmSession == null) { + return false; + } + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS + && (bufferEncrypted || !playClearSamplesWithoutKeys); + } + private void flushDecoder() { inputBuffer = null; + waitingForKeys = false; if (outputBuffer != null) { outputBuffer.release(); outputBuffer = null; @@ -278,7 +355,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public boolean isReady() { return audioTrack.hasPendingData() - || (inputFormat != null && (isSourceReady() || outputBuffer != null)); + || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null)); } @Override @@ -339,6 +416,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements outputBuffer = null; inputFormat = null; audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + waitingForKeys = false; try { if (decoder != null) { decoder.release(); @@ -347,12 +425,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } audioTrack.release(); } finally { - decoderCounters.ensureUpdated(); - eventDispatcher.disabled(decoderCounters); + try { + if (drmSession != null) { + drmSessionManager.releaseSession(drmSession); + } + } finally { + try { + if (pendingDrmSession != null && pendingDrmSession != drmSession) { + drmSessionManager.releaseSession(pendingDrmSession); + } + } finally { + drmSession = null; + pendingDrmSession = null; + decoderCounters.ensureUpdated(); + eventDispatcher.disabled(decoderCounters); + } + } } } - private boolean readFormat() { + private boolean readFormat() throws ExoPlaybackException { int result = readSource(formatHolder, null); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); @@ -361,8 +453,28 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } - private void onInputFormatChanged(Format newFormat) { + private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + Format oldFormat = inputFormat; inputFormat = newFormat; + + boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null + : oldFormat.drmInitData); + if (drmInitDataChanged) { + if (inputFormat.drmInitData != null) { + if (drmSessionManager == null) { + throw ExoPlaybackException.createForRenderer( + new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); + } + pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), + inputFormat.drmInitData); + if (pendingDrmSession == drmSession) { + drmSessionManager.releaseSession(pendingDrmSession); + } + } else { + pendingDrmSession = null; + } + } + eventDispatcher.inputFormatChanged(newFormat); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java new file mode 100644 index 0000000000..6916b972b2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java @@ -0,0 +1,20 @@ +package com.google.android.exoplayer2.drm; + +/** + * An exception when doing drm decryption using the In-App Drm + */ +public class DecryptionException extends Exception { + private final int errorCode; + + public DecryptionException(int errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * Get error code + */ + public int getErrorCode() { + return errorCode; + } +} From 0354ef24d4bf0c19c9bc42b3a6485457c955100a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 7 Nov 2016 07:47:30 -0800 Subject: [PATCH 071/206] Add support for splice info section reading in TS This CL adds a SpliceInfoDecoder for the most common SCTE35 commands for splcing. So far, it only includes TransportStreams, but porting it to HLS and DASH should be fairly easy. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138389807 --- .../ts/DefaultTsPayloadReaderFactory.java | 2 + .../extractor/ts/SpliceInfoSectionReader.java | 48 ++++ .../exoplayer2/extractor/ts/TsExtractor.java | 1 + .../metadata/scte35/PrivateCommand.java | 78 ++++++ .../metadata/scte35/SpliceCommand.java | 30 +++ .../metadata/scte35/SpliceInfoDecoder.java | 86 +++++++ .../metadata/scte35/SpliceInsertCommand.java | 193 +++++++++++++++ .../metadata/scte35/SpliceNullCommand.java | 47 ++++ .../scte35/SpliceScheduleCommand.java | 226 ++++++++++++++++++ .../metadata/scte35/TimeSignalCommand.java | 81 +++++++ .../android/exoplayer2/util/MimeTypes.java | 1 + 11 files changed, 793 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 96f7ff423f..438d426c93 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -78,6 +78,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact (flags & FLAG_DETECT_ACCESS_UNITS) != 0)); case TsExtractor.TS_STREAM_TYPE_H265: return new PesReader(new H265Reader()); + case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: + return new SectionReader(new SpliceInfoSectionReader()); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); default: diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java new file mode 100644 index 0000000000..b1e71d6651 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 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.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses splice info sections as defined by SCTE35. + */ +public final class SpliceInfoSectionReader implements SectionPayloadReader { + + private TrackOutput output; + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TsPayloadReader.TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, + Format.NO_VALUE, null)); + } + + @Override + public void consume(ParsableByteArray sectionData) { + int sampleSize = sectionData.bytesLeft(); + output.sampleData(sectionData, sampleSize); + output.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 219101b8d3..6808c14cf9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -64,6 +64,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_H264 = 0x1B; public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; + public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java new file mode 100644 index 0000000000..f75a1b46a4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Represents a private command as defined in SCTE35, Section 9.3.6. + */ +public final class PrivateCommand extends SpliceCommand { + + public final long ptsAdjustment; + public final long identifier; + + public final byte[] commandBytes; + + private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { + this.ptsAdjustment = ptsAdjustment; + this.identifier = identifier; + this.commandBytes = commandBytes; + } + + private PrivateCommand(Parcel in) { + ptsAdjustment = in.readLong(); + identifier = in.readLong(); + commandBytes = new byte[in.readInt()]; + in.readByteArray(commandBytes); + } + + /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData, + int commandLength, long ptsAdjustment) { + long identifier = sectionData.readUnsignedInt(); + byte[] privateBytes = new byte[commandLength - 4 /* identifier size */]; + sectionData.readBytes(privateBytes, 0, privateBytes.length); + return new PrivateCommand(identifier, privateBytes, ptsAdjustment); + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(ptsAdjustment); + dest.writeLong(identifier); + dest.writeInt(commandBytes.length); + dest.writeByteArray(commandBytes); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PrivateCommand createFromParcel(Parcel in) { + return new PrivateCommand(in); + } + + @Override + public PrivateCommand[] newArray(int size) { + return new PrivateCommand[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java new file mode 100644 index 0000000000..8dfa3b8942 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 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.scte35; + +import com.google.android.exoplayer2.metadata.Metadata; + +/** + * Superclass for SCTE35 splice commands. + */ +public abstract class SpliceCommand implements Metadata.Entry { + + @Override + public int describeContents() { + return 0; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java new file mode 100644 index 0000000000..5af0f25481 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.text.TextUtils; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoder; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Decodes splice info sections and produces splice commands. + */ +public final class SpliceInfoDecoder implements MetadataDecoder { + + private static final int TYPE_SPLICE_NULL = 0x00; + private static final int TYPE_SPLICE_SCHEDULE = 0x04; + private static final int TYPE_SPLICE_INSERT = 0x05; + private static final int TYPE_TIME_SIGNAL = 0x06; + private static final int TYPE_PRIVATE_COMMAND = 0xFF; + + private final ParsableByteArray sectionData; + private final ParsableBitArray sectionHeader; + + public SpliceInfoDecoder() { + sectionData = new ParsableByteArray(); + sectionHeader = new ParsableBitArray(); + } + + @Override + public boolean canDecode(String mimeType) { + return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35); + } + + @Override + public Metadata decode(byte[] data, int size) throws MetadataDecoderException { + sectionData.reset(data, size); + sectionHeader.reset(data, size); + // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2), + // section_length(12), protocol_version(8), encrypted_packet(1), encryption_algorithm(6). + sectionHeader.skipBits(39); + long ptsAdjustment = sectionHeader.readBits(1); + ptsAdjustment = (ptsAdjustment << 32) | sectionHeader.readBits(32); + // cw_index(8), tier(12). + sectionHeader.skipBits(20); + int spliceCommandLength = sectionHeader.readBits(12); + int spliceCommandType = sectionHeader.readBits(8); + SpliceCommand command = null; + // Go to the start of the command by skipping all fields up to command_type. + sectionData.skipBytes(14); + switch (spliceCommandType) { + case TYPE_SPLICE_NULL: + command = new SpliceNullCommand(); + break; + case TYPE_SPLICE_SCHEDULE: + command = SpliceScheduleCommand.parseFromSection(sectionData); + break; + case TYPE_SPLICE_INSERT: + command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment); + break; + case TYPE_TIME_SIGNAL: + command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment); + break; + case TYPE_PRIVATE_COMMAND: + command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); + break; + } + return command == null ? new Metadata() : new Metadata(command); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java new file mode 100644 index 0000000000..1e025aeb35 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a splice insert command defined in SCTE35, Section 9.3.3. + */ +public final class SpliceInsertCommand extends SpliceCommand { + + public final long spliceEventId; + public final boolean spliceEventCancelIndicator; + public final boolean outOfNetworkIndicator; + public final boolean programSpliceFlag; + public final boolean spliceImmediateFlag; + public final long programSplicePts; + public final List componentSpliceList; + public final boolean autoReturn; + public final long breakDuration; + public final int uniqueProgramId; + public final int availNum; + public final int availsExpected; + + private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, + boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, + long programSplicePts, List componentSpliceList, boolean autoReturn, + long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + this.spliceEventId = spliceEventId; + this.spliceEventCancelIndicator = spliceEventCancelIndicator; + this.outOfNetworkIndicator = outOfNetworkIndicator; + this.programSpliceFlag = programSpliceFlag; + this.spliceImmediateFlag = spliceImmediateFlag; + this.programSplicePts = programSplicePts; + this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); + this.autoReturn = autoReturn; + this.breakDuration = breakDuration; + this.uniqueProgramId = uniqueProgramId; + this.availNum = availNum; + this.availsExpected = availsExpected; + } + + private SpliceInsertCommand(Parcel in) { + spliceEventId = in.readLong(); + spliceEventCancelIndicator = in.readByte() == 1; + outOfNetworkIndicator = in.readByte() == 1; + programSpliceFlag = in.readByte() == 1; + spliceImmediateFlag = in.readByte() == 1; + programSplicePts = in.readLong(); + int componentSpliceListSize = in.readInt(); + List componentSpliceList = new ArrayList<>(componentSpliceListSize); + for (int i = 0; i < componentSpliceListSize; i++) { + componentSpliceList.add(ComponentSplice.createFromParcel(in)); + } + this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); + autoReturn = in.readByte() == 1; + breakDuration = in.readLong(); + uniqueProgramId = in.readInt(); + availNum = in.readInt(); + availsExpected = in.readInt(); + } + + /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData, + long ptsAdjustment) { + long spliceEventId = sectionData.readUnsignedInt(); + // splice_event_cancel_indicator(1), reserved(7). + boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0; + boolean outOfNetworkIndicator = false; + boolean programSpliceFlag = false; + boolean spliceImmediateFlag = false; + long programSplicePts = C.TIME_UNSET; + ArrayList componentSplices = new ArrayList<>(); + int uniqueProgramId = 0; + int availNum = 0; + int availsExpected = 0; + boolean autoReturn = false; + long duration = C.TIME_UNSET; + if (!spliceEventCancelIndicator) { + int headerByte = sectionData.readUnsignedByte(); + outOfNetworkIndicator = (headerByte & 0x80) != 0; + programSpliceFlag = (headerByte & 0x40) != 0; + boolean durationFlag = (headerByte & 0x20) != 0; + spliceImmediateFlag = (headerByte & 0x10) != 0; + if (programSpliceFlag && !spliceImmediateFlag) { + programSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment); + } + if (!programSpliceFlag) { + int componentCount = sectionData.readUnsignedByte(); + componentSplices = new ArrayList<>(componentCount); + for (int i = 0; i < componentCount; i++) { + int componentTag = sectionData.readUnsignedByte(); + long componentSplicePts = C.TIME_UNSET; + if (!spliceImmediateFlag) { + componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment); + } + componentSplices.add(new ComponentSplice(componentTag, componentSplicePts)); + } + } + if (durationFlag) { + long firstByte = sectionData.readUnsignedByte(); + autoReturn = (firstByte & 0x80) != 0; + duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + } + uniqueProgramId = sectionData.readUnsignedShort(); + availNum = sectionData.readUnsignedByte(); + availsExpected = sectionData.readUnsignedByte(); + } + return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, + programSpliceFlag, spliceImmediateFlag, programSplicePts, componentSplices, autoReturn, + duration, uniqueProgramId, availNum, availsExpected); + } + + /** + * Holds splicing information for specific splice insert command components. + */ + public static final class ComponentSplice { + + public final int componentTag; + public final long componentSplicePts; + + private ComponentSplice(int componentTag, long componentSplicePts) { + this.componentTag = componentTag; + this.componentSplicePts = componentSplicePts; + } + + public void writeToParcel(Parcel dest) { + dest.writeInt(componentTag); + dest.writeLong(componentSplicePts); + } + + public static ComponentSplice createFromParcel(Parcel in) { + return new ComponentSplice(in.readInt(), in.readLong()); + } + + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(spliceEventId); + dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0)); + dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0)); + dest.writeByte((byte) (programSpliceFlag ? 1 : 0)); + dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0)); + dest.writeLong(programSplicePts); + int componentSpliceListSize = componentSpliceList.size(); + dest.writeInt(componentSpliceListSize); + for (int i = 0; i < componentSpliceListSize; i++) { + componentSpliceList.get(i).writeToParcel(dest); + } + dest.writeByte((byte) (autoReturn ? 1 : 0)); + dest.writeLong(breakDuration); + dest.writeInt(uniqueProgramId); + dest.writeInt(availNum); + dest.writeInt(availsExpected); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public SpliceInsertCommand createFromParcel(Parcel in) { + return new SpliceInsertCommand(in); + } + + @Override + public SpliceInsertCommand[] newArray(int size) { + return new SpliceInsertCommand[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java new file mode 100644 index 0000000000..461d49ebb4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.os.Parcel; + +/** + * Represents a splice null command as defined in SCTE35, Section 9.3.1. + */ +public final class SpliceNullCommand extends SpliceCommand { + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Do nothing. + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public SpliceNullCommand createFromParcel(Parcel in) { + return new SpliceNullCommand(); + } + + @Override + public SpliceNullCommand[] newArray(int size) { + return new SpliceNullCommand[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java new file mode 100644 index 0000000000..9b391cea6c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a splice schedule command as defined in SCTE35, Section 9.3.2. + */ +public final class SpliceScheduleCommand extends SpliceCommand { + + /** + * Represents a splice event as contained in a {@link SpliceScheduleCommand}. + */ + public static final class Event { + + public final long spliceEventId; + public final boolean spliceEventCancelIndicator; + public final boolean outOfNetworkIndicator; + public final boolean programSpliceFlag; + public final long utcSpliceTime; + public final List componentSpliceList; + public final boolean autoReturn; + public final long breakDuration; + public final int uniqueProgramId; + public final int availNum; + public final int availsExpected; + + private Event(long spliceEventId, boolean spliceEventCancelIndicator, + boolean outOfNetworkIndicator, boolean programSpliceFlag, + List componentSpliceList, long utcSpliceTime, boolean autoReturn, + long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + this.spliceEventId = spliceEventId; + this.spliceEventCancelIndicator = spliceEventCancelIndicator; + this.outOfNetworkIndicator = outOfNetworkIndicator; + this.programSpliceFlag = programSpliceFlag; + this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); + this.utcSpliceTime = utcSpliceTime; + this.autoReturn = autoReturn; + this.breakDuration = breakDuration; + this.uniqueProgramId = uniqueProgramId; + this.availNum = availNum; + this.availsExpected = availsExpected; + } + + private Event(Parcel in) { + this.spliceEventId = in.readLong(); + this.spliceEventCancelIndicator = in.readByte() == 1; + this.outOfNetworkIndicator = in.readByte() == 1; + this.programSpliceFlag = in.readByte() == 1; + int componentSpliceListLength = in.readInt(); + ArrayList componentSpliceList = new ArrayList<>(componentSpliceListLength); + for (int i = 0; i < componentSpliceListLength; i++) { + componentSpliceList.add(ComponentSplice.createFromParcel(in)); + } + this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); + this.utcSpliceTime = in.readLong(); + this.autoReturn = in.readByte() == 1; + this.breakDuration = in.readLong(); + this.uniqueProgramId = in.readInt(); + this.availNum = in.readInt(); + this.availsExpected = in.readInt(); + } + + private static Event parseFromSection(ParsableByteArray sectionData) { + long spliceEventId = sectionData.readUnsignedInt(); + // splice_event_cancel_indicator(1), reserved(7). + boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0; + boolean outOfNetworkIndicator = false; + boolean programSpliceFlag = false; + long utcSpliceTime = C.TIME_UNSET; + ArrayList componentSplices = new ArrayList<>(); + int uniqueProgramId = 0; + int availNum = 0; + int availsExpected = 0; + boolean autoReturn = false; + long duration = C.TIME_UNSET; + if (!spliceEventCancelIndicator) { + int headerByte = sectionData.readUnsignedByte(); + outOfNetworkIndicator = (headerByte & 0x80) != 0; + programSpliceFlag = (headerByte & 0x40) != 0; + boolean durationFlag = (headerByte & 0x20) != 0; + if (programSpliceFlag) { + utcSpliceTime = sectionData.readUnsignedInt(); + } + if (!programSpliceFlag) { + int componentCount = sectionData.readUnsignedByte(); + componentSplices = new ArrayList<>(componentCount); + for (int i = 0; i < componentCount; i++) { + int componentTag = sectionData.readUnsignedByte(); + long componentUtcSpliceTime = sectionData.readUnsignedInt(); + componentSplices.add(new ComponentSplice(componentTag, componentUtcSpliceTime)); + } + } + if (durationFlag) { + long firstByte = sectionData.readUnsignedByte(); + autoReturn = (firstByte & 0x80) != 0; + duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + } + uniqueProgramId = sectionData.readUnsignedShort(); + availNum = sectionData.readUnsignedByte(); + availsExpected = sectionData.readUnsignedByte(); + } + return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, + programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, duration, uniqueProgramId, + availNum, availsExpected); + } + + private void writeToParcel(Parcel dest) { + dest.writeLong(spliceEventId); + dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0)); + dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0)); + dest.writeByte((byte) (programSpliceFlag ? 1 : 0)); + int componentSpliceListSize = componentSpliceList.size(); + dest.writeInt(componentSpliceListSize); + for (int i = 0; i < componentSpliceListSize; i++) { + componentSpliceList.get(i).writeToParcel(dest); + } + dest.writeLong(utcSpliceTime); + dest.writeByte((byte) (autoReturn ? 1 : 0)); + dest.writeLong(breakDuration); + dest.writeInt(uniqueProgramId); + dest.writeInt(availNum); + dest.writeInt(availsExpected); + } + + private static Event createFromParcel(Parcel in) { + return new Event(in); + } + + } + + /** + * Holds splicing information for specific splice schedule command components. + */ + public static final class ComponentSplice { + + public final int componentTag; + public final long utcSpliceTime; + + private ComponentSplice(int componentTag, long utcSpliceTime) { + this.componentTag = componentTag; + this.utcSpliceTime = utcSpliceTime; + } + + private static ComponentSplice createFromParcel(Parcel in) { + return new ComponentSplice(in.readInt(), in.readLong()); + } + + private void writeToParcel(Parcel dest) { + dest.writeInt(componentTag); + dest.writeLong(utcSpliceTime); + } + + } + + public final List events; + + private SpliceScheduleCommand(List events) { + this.events = Collections.unmodifiableList(events); + } + + private SpliceScheduleCommand(Parcel in) { + int eventsSize = in.readInt(); + ArrayList events = new ArrayList<>(eventsSize); + for (int i = 0; i < eventsSize; i++) { + events.add(Event.createFromParcel(in)); + } + this.events = Collections.unmodifiableList(events); + } + + /* package */ static SpliceScheduleCommand parseFromSection(ParsableByteArray sectionData) { + int spliceCount = sectionData.readUnsignedByte(); + ArrayList events = new ArrayList<>(spliceCount); + for (int i = 0; i < spliceCount; i++) { + events.add(Event.parseFromSection(sectionData)); + } + return new SpliceScheduleCommand(events); + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + int eventsSize = events.size(); + dest.writeInt(eventsSize); + for (int i = 0; i < eventsSize; i++) { + events.get(i).writeToParcel(dest); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public SpliceScheduleCommand createFromParcel(Parcel in) { + return new SpliceScheduleCommand(in); + } + + @Override + public SpliceScheduleCommand[] newArray(int size) { + return new SpliceScheduleCommand[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java new file mode 100644 index 0000000000..c31f4dedc8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 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.scte35; + +import android.os.Parcel; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Represents a time signal command as defined in SCTE35, Section 9.3.4. + */ +public final class TimeSignalCommand extends SpliceCommand { + + public final long ptsTime; + + private TimeSignalCommand(long ptsTime) { + this.ptsTime = ptsTime; + } + + /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData, + long ptsAdjustment) { + return new TimeSignalCommand(parseSpliceTime(sectionData, ptsAdjustment)); + } + + /** + * Parses pts_time from splice_time(), defined in Section 9.4.1. Returns {@link C#TIME_UNSET}, if + * time_specified_flag is false. + * + * @param sectionData The section data from which the pts_time is parsed. + * @param ptsAdjustment The pts adjustment provided by the splice info section header. + * @return The pts_time defined by splice_time(), or {@link C#TIME_UNSET}, if time_specified_flag + * is false. + */ + /* package */ static long parseSpliceTime(ParsableByteArray sectionData, long ptsAdjustment) { + long firstByte = sectionData.readUnsignedByte(); + long ptsTime = C.TIME_UNSET; + if ((firstByte & 0x80) != 0 /* time_specified_flag */) { + // See SCTE35 9.2.1 for more information about pts adjustment. + ptsTime = (firstByte & 0x01) << 32 | sectionData.readUnsignedInt(); + ptsTime += ptsAdjustment; + ptsTime &= 0x1FFFFFFFFL; + } + return ptsTime; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(ptsTime); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public TimeSignalCommand createFromParcel(Parcel in) { + return new TimeSignalCommand(in.readLong()); + } + + @Override + public TimeSignalCommand[] newArray(int size) { + return new TimeSignalCommand[size]; + } + + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index fb91b17cc0..641dd670af 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -74,6 +74,7 @@ public final class MimeTypes { public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc"; public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; + public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; private MimeTypes() {} From a4935a9ba0a632febcbe7a6b95e5875c2b1a4996 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Mon, 7 Nov 2016 08:11:12 -0800 Subject: [PATCH 072/206] Added support for multiple RawCC (CEA-608/CEA-708) tracks in DASH. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138392065 --- .../google/android/exoplayer2/FormatTest.java | 4 +- .../extractor/rawcc/RawCcExtractorTest.java | 17 ++-- .../dash/manifest/DashManifestParserTest.java | 32 +++++++ .../com/google/android/exoplayer2/Format.java | 84 ++++++++++++++----- .../extractor/rawcc/RawCcExtractor.java | 10 +-- .../source/dash/DefaultDashChunkSource.java | 2 +- .../dash/manifest/DashManifestParser.java | 71 ++++++++++++++-- .../text/SubtitleDecoderFactory.java | 7 +- .../exoplayer2/text/cea/Cea608Decoder.java | 40 +++++++-- 9 files changed, 218 insertions(+), 49 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index 1eb38de40f..c8c1b4ed93 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -64,8 +64,8 @@ public final class FormatTest extends TestCase { Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, - C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, - drmInitData, metadata); + C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, Format.OFFSET_SAMPLE_RELATIVE, + INIT_DATA, drmInitData, metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 2f4da01228..4e99e2745e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.rawcc; import android.annotation.TargetApi; import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; /** * Tests for {@link RawCcExtractor}. @@ -27,12 +29,15 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class RawCcExtractorTest extends InstrumentationTestCase { public void testRawCcSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { - @Override - public Extractor create() { - return new RawCcExtractor(); - } - }, "rawcc/sample.rawcc", getInstrumentation()); + TestUtil.assertOutput( + new TestUtil.ExtractorFactory() { + @Override + public Extractor create() { + return new RawCcExtractor( + Format.createTextContainerFormat(null, null, MimeTypes.APPLICATION_CEA608, + "cea608", Format.NO_VALUE, 0, null, 1)); + } + }, "rawcc/sample.rawcc", getInstrumentation()); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 460d3bb04e..944781b890 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; @@ -68,4 +69,35 @@ public class DashManifestParserTest extends InstrumentationTestCase { } } + public void testParseCea608AccessibilityChannel() { + assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng")); + assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng")); + assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng")); + assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng")); + + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null)); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("")); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng")); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng")); + assertEquals(Format.NO_VALUE, + DashManifestParser.parseCea608AccessibilityChannel("Wrong format")); + } + + public void testParseCea708AccessibilityChannel() { + assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng")); + assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng")); + assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng")); + assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng")); + assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng")); + + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null)); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel("")); + assertEquals(Format.NO_VALUE, + DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng")); + assertEquals(Format.NO_VALUE, + DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng")); + assertEquals(Format.NO_VALUE, + DashManifestParser.parseCea708AccessibilityChannel("Wrong format")); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 9528536296..14efb6a2c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -178,6 +178,11 @@ public final class Format implements Parcelable { */ public final String language; + /** + * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. + */ + public final int accessibilityChannel; + // Lazily initialized hashcode and framework media format. private int hashCode; @@ -190,7 +195,8 @@ public final class Format implements Parcelable { float frameRate, List initializationData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); + NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, + null); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, @@ -215,8 +221,8 @@ public final class Format implements Parcelable { byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData, - drmInitData, null); + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, + initializationData, drmInitData, null); } // Audio. @@ -226,8 +232,8 @@ public final class Format implements Parcelable { List initializationData, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData, - null, null); + NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, + initializationData, null, null); } public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, @@ -254,7 +260,7 @@ public final class Format implements Parcelable { @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, - encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, + encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, metadata); } @@ -263,23 +269,46 @@ public final class Format implements Parcelable { public static Format createTextContainerFormat(String id, String containerMimeType, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language) { + return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, + selectionFlags, language, NO_VALUE); + } + + public static Format createTextContainerFormat(String id, String containerMimeType, + String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, + String language, int accessibilityChannel) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null, null); + NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, + OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - drmInitData, OFFSET_SAMPLE_RELATIVE); + NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE); + } + + public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, + int bitrate, @C.SelectionFlags int selectionFlags, String language, + int accessibilityChannel, DrmInitData drmInitData) { + return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, + accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData, long subsampleOffsetUs) { + return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, + NO_VALUE, drmInitData, subsampleOffsetUs); + } + + public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, + int bitrate, @C.SelectionFlags int selectionFlags, String language, + int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, subsampleOffsetUs, null, drmInitData, null); + NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null, + drmInitData, null); } // Image. @@ -288,7 +317,8 @@ public final class Format implements Parcelable { int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); + NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, + null); } // Generic. @@ -297,14 +327,14 @@ public final class Format implements Parcelable { String sampleMimeType, int bitrate) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, null, null); + NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); + NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, @@ -312,8 +342,8 @@ public final class Format implements Parcelable { float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, - long subsampleOffsetUs, List initializationData, DrmInitData drmInitData, - Metadata metadata) { + int accessibilityChannel, long subsampleOffsetUs, List initializationData, + DrmInitData drmInitData, Metadata metadata) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -334,6 +364,7 @@ public final class Format implements Parcelable { this.encoderPadding = encoderPadding; this.selectionFlags = selectionFlags; this.language = language; + this.accessibilityChannel = accessibilityChannel; this.subsampleOffsetUs = subsampleOffsetUs; this.initializationData = initializationData == null ? Collections.emptyList() : initializationData; @@ -364,6 +395,7 @@ public final class Format implements Parcelable { encoderPadding = in.readInt(); selectionFlags = in.readInt(); language = in.readString(); + accessibilityChannel = in.readInt(); subsampleOffsetUs = in.readLong(); int initializationDataSize = in.readInt(); initializationData = new ArrayList<>(initializationDataSize); @@ -378,14 +410,16 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, @@ -393,7 +427,8 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithManifestFormatInfo(Format manifestFormat, @@ -409,28 +444,32 @@ public final class Format implements Parcelable { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, subsampleOffsetUs, initializationData, drmInitData, metadata); + language, accessibilityChannel, subsampleOffsetUs, initializationData, drmInitData, + metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithMetadata(Metadata metadata) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData, metadata); + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } /** @@ -489,6 +528,7 @@ public final class Format implements Parcelable { result = 31 * result + channelCount; result = 31 * result + sampleRate; result = 31 * result + (language == null ? 0 : language.hashCode()); + result = 31 * result + accessibilityChannel; result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); hashCode = result; @@ -514,6 +554,7 @@ public final class Format implements Parcelable { || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id) || !Util.areEqual(language, other.language) + || accessibilityChannel != other.accessibilityChannel || !Util.areEqual(containerMimeType, other.containerMimeType) || !Util.areEqual(sampleMimeType, other.sampleMimeType) || !Util.areEqual(codecs, other.codecs) @@ -584,6 +625,7 @@ public final class Format implements Parcelable { dest.writeInt(encoderPadding); dest.writeInt(selectionFlags); dest.writeString(language); + dest.writeInt(accessibilityChannel); dest.writeLong(subsampleOffsetUs); int initializationDataSize = initializationData.size(); dest.writeInt(initializationDataSize); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 452d09e132..8405f21756 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -45,6 +44,8 @@ public final class RawCcExtractor implements Extractor { private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1; private static final int STATE_READING_SAMPLES = 2; + private final Format format; + private final ParsableByteArray dataScratch; private TrackOutput trackOutput; @@ -55,7 +56,8 @@ public final class RawCcExtractor implements Extractor { private int remainingSampleCount; private int sampleBytesWritten; - public RawCcExtractor() { + public RawCcExtractor(Format format) { + this.format = format; dataScratch = new ParsableByteArray(SCRATCH_SIZE); parserState = STATE_READING_HEADER; } @@ -65,9 +67,7 @@ public final class RawCcExtractor implements Extractor { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); trackOutput = output.track(0); output.endTracks(); - - trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, - null, Format.NO_VALUE, 0, null, null)); + trackOutput.format(format); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 83b8724170..919a0231ea 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -349,7 +349,7 @@ public class DefaultDashChunkSource implements DashChunkSource { boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { - extractor = new RawCcExtractor(); + extractor = new RawCcExtractor(representation.format); resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index fdfe8cb4b3..18e81e53b2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -59,6 +59,10 @@ public class DashManifestParser extends DefaultHandler private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("(\\d+)(?:/(\\d+))?"); + private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile("CC([1-4])=.*"); + private static final Pattern CEA_708_ACCESSIBILITY_PATTERN = + Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*"); + private final String contentId; private final XmlPullParserFactory xmlParserFactory; @@ -235,6 +239,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); + int accessibilityChannel = Format.NO_VALUE; ArrayList drmSchemeDatas = new ArrayList<>(); List representationInfos = new ArrayList<>(); @@ -256,12 +261,15 @@ public class DashManifestParser extends DefaultHandler contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, - width, height, frameRate, audioChannels, audioSamplingRate, language, segmentBase); + width, height, frameRate, audioChannels, audioSamplingRate, language, + accessibilityChannel, segmentBase); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { + accessibilityChannel = parseAccessibilityValue(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { @@ -365,7 +373,8 @@ public class DashManifestParser extends DefaultHandler protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseUrl, String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, - int adaptationSetAudioSamplingRate, String adaptationSetLanguage, SegmentBase segmentBase) + int adaptationSetAudioSamplingRate, String adaptationSetLanguage, + int adaptationSetAccessibilityChannel, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -404,7 +413,8 @@ public class DashManifestParser extends DefaultHandler } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, - audioSamplingRate, bandwidth, adaptationSetLanguage, codecs); + audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetAccessibilityChannel, + codecs); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas); @@ -412,7 +422,7 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - String codecs) { + int accessiblityChannel, String codecs) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { if (MimeTypes.isVideo(sampleMimeType)) { @@ -423,7 +433,10 @@ public class DashManifestParser extends DefaultHandler bitrate, audioChannels, audioSamplingRate, null, 0, language); } else if (mimeTypeIsRawText(sampleMimeType)) { return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, 0, language); + bitrate, 0, language, accessiblityChannel); + } else if (containerMimeType.equals(MimeTypes.APPLICATION_RAWCC)) { + return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, + bitrate, 0, language, accessiblityChannel); } else { return Format.createContainerFormat(id, containerMimeType, codecs, sampleMimeType, bitrate); } @@ -726,6 +739,54 @@ public class DashManifestParser extends DefaultHandler } } + private static int parseAccessibilityValue(XmlPullParser xpp) + throws IOException, XmlPullParserException { + String schemeIdUri = parseString(xpp, "schemeIdUri", null); + String valueString = parseString(xpp, "value", null); + int accessibilityValue; + if (schemeIdUri == null || valueString == null) { + accessibilityValue = Format.NO_VALUE; + } else if ("urn:scte:dash:cc:cea-608:2015".equals(schemeIdUri)) { + accessibilityValue = parseCea608AccessibilityChannel(valueString); + } else if ("urn:scte:dash:cc:cea-708:2015".equals(schemeIdUri)) { + accessibilityValue = parseCea708AccessibilityChannel(valueString); + } else { + accessibilityValue = Format.NO_VALUE; + } + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, "Accessibility")); + return accessibilityValue; + } + + static int parseCea608AccessibilityChannel(String accessibilityValueString) { + if (accessibilityValueString == null) { + return Format.NO_VALUE; + } + Matcher accessibilityValueMatcher = + CEA_608_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse channel number from " + accessibilityValueString); + return Format.NO_VALUE; + } + } + + static int parseCea708AccessibilityChannel(String accessibilityValueString) { + if (accessibilityValueString == null) { + return Format.NO_VALUE; + } + Matcher accessibilityValueMatcher = + CEA_708_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse service block number from " + accessibilityValueString); + return Format.NO_VALUE; + } + } + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { float frameRate = defaultValue; String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index d1afbe86a8..d1e474d434 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -74,7 +74,12 @@ public interface SubtitleDecoderFactory { if (clazz == null) { throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } - return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); + if (clazz == Cea608Decoder.class) { + return clazz.asSubclass(SubtitleDecoder.class) + .getConstructor(Integer.TYPE).newInstance(format.accessibilityChannel); + } else { + return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); + } } catch (Exception e) { throw new IllegalStateException("Unexpected error instantiating decoder", e); } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 5ff68c2781..c33d2abb89 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.cea; import android.text.TextUtils; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoder; @@ -27,9 +28,15 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class Cea608Decoder extends CeaDecoder { - private static final int NTSC_CC_FIELD_1 = 0x00; - private static final int CC_TYPE_MASK = 0x03; + private static final String TAG = "Cea608Decoder"; + private static final int CC_VALID_FLAG = 0x04; + private static final int CC_TYPE_FLAG = 0x02; + private static final int CC_FIELD_FLAG = 0x01; + + private static final int NTSC_CC_FIELD_1 = 0x00; + private static final int NTSC_CC_FIELD_2 = 0x01; + private static final int CC_VALID_608_ID = 0x04; private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; @@ -160,6 +167,8 @@ public final class Cea608Decoder extends CeaDecoder { private final StringBuilder captionStringBuilder; + private final int selectedField; + private int captionMode; private int captionRowCount; private String captionString; @@ -170,10 +179,21 @@ public final class Cea608Decoder extends CeaDecoder { private byte repeatableControlCc1; private byte repeatableControlCc2; - public Cea608Decoder() { + public Cea608Decoder(int accessibilityChannel) { ccData = new ParsableByteArray(); captionStringBuilder = new StringBuilder(); + switch (accessibilityChannel) { + case 3: + case 4: + selectedField = 2; + break; + case 1: + case 2: + case Format.NO_VALUE: + default: + selectedField = 1; + } setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; @@ -219,14 +239,18 @@ public final class Cea608Decoder extends CeaDecoder { boolean captionDataProcessed = false; boolean isRepeatableControl = false; while (ccData.bytesLeft() > 0) { - byte ccTypeAndValid = (byte) (ccData.readUnsignedByte() & 0x07); + byte ccDataHeader = (byte) ccData.readUnsignedByte(); byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); - // Only examine valid NTSC_CC_FIELD_1 packets - if ((ccTypeAndValid & CC_VALID_FLAG) == 0 - || (ccTypeAndValid & CC_TYPE_MASK) != NTSC_CC_FIELD_1) { - // TODO: Add support for NTSC_CC_FIELD_2 packets + // Only examine valid CEA-608 packets + if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) { + continue; + } + + // Only examine packets within the selected field + if ((selectedField == 1 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_1) + || (selectedField == 2 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_2)) { continue; } From 3c8db3b9cbf85038b7df0b1a084f3b625e3a781e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Nov 2016 09:43:02 -0800 Subject: [PATCH 073/206] Pass Timeline back with seek position This change fixes the race condition where the internal timeline is different to the externally visible timeline when a seek is performed. We now resolve the seek position using the externally visible timeline, then adjust the period index as required for the internal timeline. If the period is missing we follow similar logic for the existing case where the playing period is removed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138402076 --- .../android/exoplayer2/ExoPlayerImpl.java | 7 +- .../exoplayer2/ExoPlayerImplInternal.java | 132 +++++++++++++++--- 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 7c973ca995..599f1652f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -177,14 +177,17 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void seekTo(int windowIndex, long positionMs) { + if (windowIndex < 0 || (timeline != null && windowIndex >= timeline.getWindowCount())) { + throw new IndexOutOfBoundsException(); + } pendingSeekAcks++; maskingWindowIndex = windowIndex; if (positionMs == C.TIME_UNSET) { maskingWindowPositionMs = 0; - internalPlayer.seekTo(windowIndex, C.TIME_UNSET); + internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET); } else { maskingWindowPositionMs = positionMs; - internalPlayer.seekTo(windowIndex, C.msToUs(positionMs)); + internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 7d24c1b0d5..41b477b171 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -140,8 +140,7 @@ import java.io.IOException; private long elapsedRealtimeUs; private int pendingInitialSeekCount; - private int pendingSeekWindowIndex; - private long pendingSeekWindowPositionUs; + private SeekPosition pendingSeekPosition; private long rendererPositionUs; private boolean isTimelineReady; @@ -192,8 +191,9 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } - public void seekTo(int windowIndex, long positionUs) { - handler.obtainMessage(MSG_SEEK_TO, windowIndex, 0, positionUs).sendToTarget(); + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { + handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) + .sendToTarget(); } public void stop() { @@ -286,7 +286,7 @@ import java.io.IOException; return true; } case MSG_SEEK_TO: { - seekToInternal(msg.arg1, (Long) msg.obj); + seekToInternal((SeekPosition) msg.obj); return true; } case MSG_STOP: { @@ -513,15 +513,21 @@ import java.io.IOException; } } - private void seekToInternal(int windowIndex, long positionUs) throws ExoPlaybackException { + private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { if (timeline == null) { pendingInitialSeekCount++; - pendingSeekWindowIndex = windowIndex; - pendingSeekWindowPositionUs = positionUs; + pendingSeekPosition = seekPosition; + return; + } + + Pair periodPosition = resolveSeekPosition(seekPosition); + if (periodPosition == null) { + // TODO: We should probably propagate an error here. + // We failed to resolve the seek position. Stop the player. + stopInternal(); return; } - Pair periodPosition = getPeriodPosition(windowIndex, positionUs); int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; @@ -826,19 +832,25 @@ import java.io.IOException; this.timeline = timelineAndManifest.first; if (pendingInitialSeekCount > 0) { - Pair periodPosition = getPeriodPosition(pendingSeekWindowIndex, - pendingSeekWindowPositionUs); + Pair periodPosition = resolveSeekPosition(pendingSeekPosition); + if (periodPosition == null) { + // TODO: We should probably propagate an error here. + // We failed to resolve the seek position. Stop the player. + stopInternal(); + return; + } playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); eventHandler.obtainMessage(MSG_SEEK_ACK, pendingInitialSeekCount, 0, playbackInfo) .sendToTarget(); pendingInitialSeekCount = 0; + pendingSeekPosition = null; } // Update the loaded periods to take into account the new timeline. if (playingPeriodHolder != null) { int index = timeline.getIndexOfPeriod(playingPeriodHolder.uid); if (index == C.INDEX_UNSET) { - attemptRestart(timeline, oldTimeline, playingPeriodHolder.index); + attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); return; } @@ -893,7 +905,7 @@ import java.io.IOException; Object uid = loadingPeriodHolder.uid; int index = timeline.getIndexOfPeriod(uid); if (index == C.INDEX_UNSET) { - attemptRestart(timeline, oldTimeline, loadingPeriodHolder.index); + attemptRestart(loadingPeriodHolder.index, oldTimeline, timeline); return; } else { int windowIndex = timeline.getPeriod(index, this.period).windowIndex; @@ -915,14 +927,10 @@ import java.io.IOException; } } - private void attemptRestart(Timeline newTimeline, Timeline oldTimeline, int oldPeriodIndex) { - int newPeriodIndex = C.INDEX_UNSET; - while (newPeriodIndex == C.INDEX_UNSET - && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { - newPeriodIndex = - newTimeline.getIndexOfPeriod(oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); - } + private void attemptRestart(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { + int newPeriodIndex = resolveSubsequentPeriod(oldPeriodIndex, oldTimeline, newTimeline); if (newPeriodIndex == C.INDEX_UNSET) { + // TODO: We should probably propagate an error here. // We failed to find a replacement period. Stop the player. stopInternal(); return; @@ -946,15 +954,83 @@ import java.io.IOException; eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } + /** + * Given a period index into an old timeline, finds the first subsequent period that also exists + * in a new timeline. The index of this period in the new timeline is returned. + * + * @param oldPeriodIndex The index of the period in the old timeline. + * @param oldTimeline The old timeline. + * @param newTimeline The new timeline. + * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET} + * if no such period was found. + */ + private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, + Timeline newTimeline) { + int newPeriodIndex = C.INDEX_UNSET; + while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { + newPeriodIndex = newTimeline.getIndexOfPeriod( + oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); + } + return newPeriodIndex; + } + + /** + * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the + * internal timeline. + * + * @param seekPosition The position to resolve. + * @return The resolved position, or null if resolution was not successful. + */ + private Pair resolveSeekPosition(SeekPosition seekPosition) { + Timeline seekTimeline = seekPosition.timeline; + if (seekTimeline == null) { + // The application performed a blind seek without a timeline (most likely based on knowledge + // of what the timeline will be). Use the internal timeline. + seekTimeline = timeline; + Assertions.checkIndex(seekPosition.windowIndex, 0, timeline.getWindowCount()); + } + // Map the SeekPosition to a position in the corresponding timeline. + Pair periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, + seekPosition.windowPositionUs); + if (timeline == seekTimeline) { + // Our internal timeline is the seek timeline, so the mapped position is correct. + return periodPosition; + } + // Attempt to find the mapped period in the internal timeline. + int periodIndex = timeline.getIndexOfPeriod( + seekTimeline.getPeriod(periodPosition.first, period, true).uid); + if (periodIndex != C.INDEX_UNSET) { + // We successfully located the period in the internal timeline. + return Pair.create(periodIndex, periodPosition.second); + } + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } + // We didn't find one. Give up. + return null; + } + + /** + * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline. + */ + private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { + return getPeriodPosition(timeline, windowIndex, windowPositionUs); + } + /** * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). * + * @param timeline The timeline containing the window. * @param windowIndex The window index. * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default * start position. * @return The corresponding (periodIndex, periodPositionUs). */ - private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { + private Pair getPeriodPosition(Timeline timeline, int windowIndex, + long windowPositionUs) { timeline.getWindow(windowIndex, window); int periodIndex = window.firstPeriodIndex; long periodPositionUs = window.getPositionInFirstPeriodUs() @@ -1344,4 +1420,18 @@ import java.io.IOException; } + private static final class SeekPosition { + + public final Timeline timeline; + public final int windowIndex; + public final long windowPositionUs; + + public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) { + this.timeline = timeline; + this.windowIndex = windowIndex; + this.windowPositionUs = windowPositionUs; + } + + } + } From 89ad5e6db3cbb1f09e38e6f68d0f590f7ae0e101 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Nov 2016 10:22:47 -0800 Subject: [PATCH 074/206] Fix ExoPlayerImplInternal timestamp conversions This fixes VOD->Live transitions, with the caveat that the Live portion still ends up further behind the live edge than intended. This will be fixed in a following change. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138407066 --- .../exoplayer2/ExoPlayerImplInternal.java | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 41b477b171..2f40985171 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -414,7 +414,7 @@ import java.io.IOException; } else { rendererPositionUs = standaloneMediaClock.getPositionUs(); } - periodPositionUs = rendererPositionUs - playingPeriodHolder.rendererPositionOffsetUs; + periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); } playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; @@ -547,13 +547,6 @@ import java.io.IOException; private long seekToPeriodPosition(int periodIndex, long periodPositionUs) throws ExoPlaybackException { - if (mediaSource == null) { - if (periodPositionUs != C.TIME_UNSET) { - resetRendererPosition(periodPositionUs); - } - return periodPositionUs; - } - stopRenderers(); rebuffering = false; setState(ExoPlayer.STATE_BUFFERING); @@ -622,9 +615,8 @@ import java.io.IOException; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - long periodOffsetUs = playingPeriodHolder == null ? 0 - : playingPeriodHolder.rendererPositionOffsetUs; - rendererPositionUs = periodOffsetUs + periodPositionUs; + rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + : playingPeriodHolder.toRendererTime(periodPositionUs); standaloneMediaClock.setPositionUs(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); @@ -769,7 +761,7 @@ import java.io.IOException; renderer.disable(); } else if (streamResetFlags[i]) { // The renderer will continue to consume from its current stream, but needs to be reset. - renderer.resetPosition(playbackInfo.positionUs); + renderer.resetPosition(rendererPositionUs); } } } @@ -785,9 +777,11 @@ import java.io.IOException; bufferAheadPeriodCount--; } loadingPeriodHolder.next = null; - long loadingPeriodPositionUs = Math.max(0, - rendererPositionUs - loadingPeriodHolder.rendererPositionOffsetUs); - loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, loadControl, false); + if (loadingPeriodHolder.prepared) { + long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, + loadingPeriodHolder.toPeriodTime(rendererPositionUs)); + loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, loadControl, false); + } } maybeContinueLoading(); updatePlaybackPositions(); @@ -798,10 +792,9 @@ import java.io.IOException; if (loadingPeriodHolder == null) { return false; } - long loadingPeriodPositionUs = rendererPositionUs - - loadingPeriodHolder.rendererPositionOffsetUs; - long loadingPeriodBufferedPositionUs = - !loadingPeriodHolder.prepared ? 0 : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); + long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared + ? loadingPeriodHolder.startPositionUs + : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { if (loadingPeriodHolder.isLast) { return true; @@ -810,7 +803,8 @@ import java.io.IOException; .getDurationUs(); } return loadControl.shouldStartPlayback( - loadingPeriodBufferedPositionUs - loadingPeriodPositionUs, rebuffering); + loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), + rebuffering); } private void maybeThrowPeriodPrepareError() throws IOException { @@ -1084,7 +1078,10 @@ import java.io.IOException; if (loadingPeriodHolder != null) { loadingPeriodHolder.setNext(newPeriodHolder); newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs(); + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs; + } else { + newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; } bufferAheadPeriodCount++; loadingPeriodHolder = newPeriodHolder; @@ -1148,7 +1145,7 @@ import java.io.IOException; formats[j] = newSelection.getFormat(j); } renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], - readingPeriodHolder.rendererPositionOffsetUs); + readingPeriodHolder.getRendererOffset()); } else { // The renderer will be disabled when transitioning to playing the next period. Mark the // SampleStream as final to play out any remaining data. @@ -1168,7 +1165,6 @@ import java.io.IOException; if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); if (playbackInfo.startPositionUs == C.TIME_UNSET) { // Update the playback info when seeking to a default position. @@ -1191,9 +1187,10 @@ import java.io.IOException; private void maybeContinueLoading() { long nextLoadPositionUs = loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { - long loadingPeriodPositionUs = rendererPositionUs - - loadingPeriodHolder.rendererPositionOffsetUs; + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + setIsLoading(false); + } else { + long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs); long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); setIsLoading(continueLoading); @@ -1203,8 +1200,6 @@ import java.io.IOException; } else { loadingPeriodHolder.needsContinueLoading = true; } - } else { - setIsLoading(false); } } @@ -1216,6 +1211,12 @@ import java.io.IOException; } private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { + boolean isFirstPeriod = playingPeriodHolder == null; + playingPeriodHolder = periodHolder; + if (isFirstPeriod) { + resetRendererPosition(playingPeriodHolder.startPositionUs); + } + int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -1239,7 +1240,6 @@ import java.io.IOException; } eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); - playingPeriodHolder = periodHolder; enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1273,7 +1273,7 @@ import java.io.IOException; } // Enable the renderer. renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs, - joining, playingPeriodHolder.rendererPositionOffsetUs); + joining, playingPeriodHolder.getRendererOffset()); MediaClock mediaClock = renderer.getMediaClock(); if (mediaClock != null) { if (rendererMediaClock != null) { @@ -1336,6 +1336,18 @@ import java.io.IOException; startPositionUs = positionUs; } + public long toRendererTime(long periodTimeUs) { + return periodTimeUs + getRendererOffset(); + } + + public long toPeriodTime(long rendererTimeUs) { + return rendererTimeUs - getRendererOffset(); + } + + public long getRendererOffset() { + return rendererPositionOffsetUs - startPositionUs; + } + public void setNext(MediaPeriodHolder next) { this.next = next; } From 7b0effc2d0558ded401cf40ec7fe55f10bdb0624 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Nov 2016 02:12:59 -0800 Subject: [PATCH 075/206] Project default start pos to fix VOD->Live transitions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138493584 --- .../exoplayer2/ExoPlayerImplInternal.java | 86 +++++++++++++------ .../google/android/exoplayer2/Timeline.java | 28 +++++- .../source/ConcatenatingMediaSource.java | 6 +- .../exoplayer2/source/LoopingMediaSource.java | 6 +- .../source/SinglePeriodTimeline.java | 11 ++- .../source/dash/DashMediaSource.java | 82 +++++++++++------- 6 files changed, 151 insertions(+), 68 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 2f40985171..35f5f393be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1014,6 +1014,15 @@ import java.io.IOException; return getPeriodPosition(timeline, windowIndex, windowPositionUs); } + /** + * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position + * projection. + */ + private Pair getPeriodPosition(Timeline timeline, int windowIndex, + long windowPositionUs) { + return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); + } + /** * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). * @@ -1021,14 +1030,23 @@ import java.io.IOException; * @param windowIndex The window index. * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default * start position. - * @return The corresponding (periodIndex, periodPositionUs). + * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the + * duration into the future by which the window's position should be projected. + * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} + * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's + * position could not be projected by {@code defaultPositionProjectionUs}. */ private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs) { - timeline.getWindow(windowIndex, window); + long windowPositionUs, long defaultPositionProjectionUs) { + timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); + if (windowPositionUs == C.TIME_UNSET) { + windowPositionUs = window.getDefaultPositionUs(); + if (windowPositionUs == C.TIME_UNSET) { + return null; + } + } int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() - + (windowPositionUs == C.TIME_UNSET ? window.getDefaultPositionUs() : windowPositionUs); + long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { @@ -1062,30 +1080,44 @@ import java.io.IOException; : (isFirstPeriodInWindow ? C.TIME_UNSET : 0); if (periodStartPositionUs == C.TIME_UNSET) { // This is the first period of a new window or we don't have a start position, so seek to - // the default position for the window. - Pair defaultPosition = getPeriodPosition(windowIndex, C.TIME_UNSET); - newLoadingPeriodIndex = defaultPosition.first; - periodStartPositionUs = defaultPosition.second; + // the default position for the window. If we're buffering ahead we also project the + // default position so that it's correct for starting playing the buffered duration of + // time in the future. + long defaultPositionProjectionUs = loadingPeriodHolder == null ? 0 + : (loadingPeriodHolder.rendererPositionOffsetUs + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs - rendererPositionUs); + Pair defaultPosition = getPeriodPosition(timeline, windowIndex, + C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + newLoadingPeriodIndex = C.INDEX_UNSET; + periodStartPositionUs = C.TIME_UNSET; + } else { + newLoadingPeriodIndex = defaultPosition.first; + periodStartPositionUs = defaultPosition.second; + } } - Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid; - MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, - loadControl.getAllocator(), periodStartPositionUs); - newMediaPeriod.prepare(this); - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); - timeline.getWindow(windowIndex, window); - newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.setNext(newPeriodHolder); - newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - loadingPeriodHolder.startPositionUs; - } else { - newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; + if (newLoadingPeriodIndex != C.INDEX_UNSET) { + Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid; + MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, + loadControl.getAllocator(), periodStartPositionUs); + newMediaPeriod.prepare(this); + MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, + trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); + timeline.getWindow(windowIndex, window); + newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); + if (loadingPeriodHolder != null) { + loadingPeriodHolder.setNext(newPeriodHolder); + newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs; + } else { + newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; + } + bufferAheadPeriodCount++; + loadingPeriodHolder = newPeriodHolder; + setIsLoading(true); } - bufferAheadPeriodCount++; - loadingPeriodHolder = newPeriodHolder; - setIsLoading(true); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/Timeline.java index b394ecabf8..7d3ad1feae 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -114,10 +114,26 @@ public abstract class Timeline { * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. + * null. The caller should pass false for efficiency reasons unless the field is required. * @return The populated {@link Window}, for convenience. */ - public abstract Window getWindow(int windowIndex, Window window, boolean setIds); + public Window getWindow(int windowIndex, Window window, boolean setIds) { + return getWindow(windowIndex, window, setIds, 0); + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to + * null. The caller should pass false for efficiency reasons unless the field is required. + * @param defaultPositionProjectionUs A duration into the future that the populated window's + * default start position should be projected. + * @return The populated {@link Window}, for convenience. + */ + public abstract Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs); /** * Returns the number of periods in the timeline. @@ -231,7 +247,9 @@ public abstract class Timeline { /** * Returns the default position relative to the start of the window at which to begin playback, - * in milliseconds. + * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. */ public long getDefaultPositionMs() { return C.usToMs(defaultPositionUs); @@ -239,7 +257,9 @@ public abstract class Timeline { /** * Returns the default position relative to the start of the window at which to begin playback, - * in microseconds. + * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. */ public long getDefaultPositionUs() { return defaultPositionUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 3b743c5fda..678ae8b06c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -171,11 +171,13 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { int sourceIndex = getSourceIndexForWindow(windowIndex); int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds); + timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds, + defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInSource; window.lastPeriodIndex += firstPeriodIndexInSource; return window; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 21455ed89d..938051843d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -118,8 +118,10 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { - childTimeline.getWindow(windowIndex % childWindowCount, window, setIds); + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + childTimeline.getWindow(windowIndex % childWindowCount, window, setIds, + defaultPositionProjectionUs); int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; window.firstPeriodIndex += periodIndexOffset; window.lastPeriodIndex += periodIndexOffset; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index f298d04432..ae367ef14c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -74,9 +74,18 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; + long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; + if (isDynamic) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } + } return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic, windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index ec794a534d..3fde4f9c8f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -400,39 +399,13 @@ public final class DashMediaSource implements MediaSource { ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; } // Snap the default position to the start of the segment containing it. - long defaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); - if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { + windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); + if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { // The default start position is too close to the start of the live window. Set it to the // minimum default start position provided the window is at least twice as big. Else set // it to the middle of the window. - defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); - } - - int periodIndex = 0; - long defaultStartPositionInPeriodUs = currentStartTimeUs + defaultStartPositionUs; - long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - while (periodIndex < manifest.getPeriodCount() - 1 - && defaultStartPositionInPeriodUs >= periodDurationUs) { - defaultStartPositionInPeriodUs -= periodDurationUs; - periodIndex++; - periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - } - Period period = manifest.getPeriod(periodIndex); - int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); - if (videoAdaptationSetIndex != C.INDEX_UNSET) { - // If there are multiple video adaptation sets with unaligned segments, the initial time may - // not correspond to the start of a segment in both, but this is an edge case. - DashSegmentIndex index = - period.adaptationSets.get(videoAdaptationSetIndex).representations.get(0).getIndex(); - if (index != null) { - int segmentNum = index.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); - windowDefaultStartPositionUs = - defaultStartPositionUs - defaultStartPositionInPeriodUs + index.getTimeUs(segmentNum); - } else { - windowDefaultStartPositionUs = defaultStartPositionUs; - } - } else { - windowDefaultStartPositionUs = defaultStartPositionUs; + windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, + windowDurationUs / 2); } } long windowStartTimeMs = manifest.availabilityStartTime @@ -561,8 +534,11 @@ public final class DashMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIdentifier) { + public Window getWindow(int windowIndex, Window window, boolean setIdentifier, + long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); + long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( + defaultPositionProjectionUs); return window.set(null, presentationStartTimeMs, windowStartTimeMs, true /* isSeekable */, manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, 0, manifest.getPeriodCount() - 1, offsetInFirstPeriodUs); @@ -578,6 +554,48 @@ public final class DashMediaSource implements MediaSource { ? C.INDEX_UNSET : (periodId - firstPeriodId); } + private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { + long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; + if (!manifest.dynamic) { + return windowDefaultStartPositionUs; + } + if (defaultPositionProjectionUs > 0) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. + return C.TIME_UNSET; + } + } + // Attempt to snap to the start of the corresponding video segment. + int periodIndex = 0; + long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs; + long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + while (periodIndex < manifest.getPeriodCount() - 1 + && defaultStartPositionInPeriodUs >= periodDurationUs) { + defaultStartPositionInPeriodUs -= periodDurationUs; + periodIndex++; + periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + } + com.google.android.exoplayer2.source.dash.manifest.Period period = + manifest.getPeriod(periodIndex); + int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); + if (videoAdaptationSetIndex == C.INDEX_UNSET) { + // No video adaptation set for snapping. + return windowDefaultStartPositionUs; + } + // If there are multiple video adaptation sets with unaligned segments, the initial time may + // not correspond to the start of a segment in both, but this is an edge case. + DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) + .representations.get(0).getIndex(); + if (snapIndex == null) { + // Video adaptation set does not include an index for snapping. + return windowDefaultStartPositionUs; + } + int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); + return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum) + - defaultStartPositionInPeriodUs; + } + } private final class ManifestCallback implements From d5cbb101ed291cadc574f83f3d09e4f7efe2dfd2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Nov 2016 09:41:45 -0800 Subject: [PATCH 076/206] SimpleExoPlayerView: Remove a bit of dead code These variables are never read, since the underlying control view reads them directly from the attrs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138528246 --- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index d494ab2a10..3d99890b6b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -189,8 +189,6 @@ public final class SimpleExoPlayerView extends FrameLayout { boolean useController = true; int surfaceType = SURFACE_TYPE_SURFACE_VIEW; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - int rewindMs = PlaybackControlView.DEFAULT_REWIND_MS; - int fastForwardMs = PlaybackControlView.DEFAULT_FAST_FORWARD_MS; int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, @@ -202,9 +200,6 @@ public final class SimpleExoPlayerView extends FrameLayout { useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); - rewindMs = a.getInt(R.styleable.SimpleExoPlayerView_rewind_increment, rewindMs); - fastForwardMs = a.getInt(R.styleable.SimpleExoPlayerView_fastforward_increment, - fastForwardMs); controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); } finally { From 8236efe6a544fc81b151fb42d7adf140e93902fc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 9 Nov 2016 00:59:00 -0800 Subject: [PATCH 077/206] Provide an overlay FrameLayout in SimpleExoPlayerView. This can be used by the app for showing arbitrary UI on top of the player (for example, UI elements associated with an ad). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138610733 --- .../exoplayer2/ui/SimpleExoPlayerView.java | 25 +++++++++++++++++-- .../res/layout/exo_simple_player_view.xml | 4 +++ library/src/main/res/values/ids.xml | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 3d99890b6b..5acb3bfb45 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -141,6 +141,12 @@ import java.util.List; *

  • Type: {@link View}
  • * * + *
  • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which + * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. + *
      + *
    • Type: {@link FrameLayout}
    • + *
    + *
  • * *

    * All child views are optional and so can be omitted if not required, however where defined they @@ -167,6 +173,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private final SubtitleView subtitleView; private final PlaybackControlView controller; private final ComponentListener componentListener; + private final FrameLayout overlayFrameLayout; private SimpleExoPlayer player; private boolean useController; @@ -231,6 +238,9 @@ public final class SimpleExoPlayerView extends FrameLayout { surfaceView = null; } + // Overlay frame layout. + overlayFrameLayout = (FrameLayout) findViewById(R.id.exo_overlay); + // Artwork view. artworkView = (ImageView) findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; @@ -421,15 +431,26 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * Get the view onto which video is rendered. This is either a {@link SurfaceView} (default) + * Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default) * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. * - * @return either a {@link SurfaceView} or a {@link TextureView}. + * @return Either a {@link SurfaceView} or a {@link TextureView}. */ public View getVideoSurfaceView() { return surfaceView; } + /** + * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of + * the player. + * + * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and + * the overlay is not present. + */ + public FrameLayout getOverlayFrameLayout() { + return overlayFrameLayout; + } + @Override public boolean onTouchEvent(MotionEvent ev) { if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index c4f34ef285..b21b0d2bd6 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -44,4 +44,8 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml index 5c533ef8d4..f55c8f6945 100644 --- a/library/src/main/res/values/ids.xml +++ b/library/src/main/res/values/ids.xml @@ -19,6 +19,7 @@ + From b1fe274df39a9f964e9616f55f935fe4d309602a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 9 Nov 2016 05:36:37 -0800 Subject: [PATCH 078/206] Replace java.text.ParseException for ExoPlayer's ParserException ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138628300 --- .../com/google/android/exoplayer2/util/UtilTest.java | 3 +-- .../exoplayer2/source/dash/DashMediaSource.java | 10 +++------- .../source/dash/manifest/DashManifestParser.java | 7 +++---- .../java/com/google/android/exoplayer2/util/Util.java | 7 ++++--- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index e3d681d6dd..8a5a8b3d0e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.util; import com.google.android.exoplayer2.testutil.TestUtil; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -140,7 +139,7 @@ public class UtilTest extends TestCase { assertEquals(1500L, Util.parseXsDuration("PT1.500S")); } - public void testParseXsDateTime() throws ParseException { + public void testParseXsDateTime() throws Exception { assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 3fde4f9c8f..e6bf8b5c02 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -314,8 +314,8 @@ public final class DashMediaSource implements MediaSource { try { long utcTimestamp = Util.parseXsDateTime(timingElement.value); onUtcTimestampResolved(utcTimestamp - manifestLoadEndTimestamp); - } catch (ParseException e) { - onUtcTimestampResolutionError(new ParserException(e)); + } catch (ParserException e) { + onUtcTimestampResolutionError(e); } } @@ -648,11 +648,7 @@ public final class DashMediaSource implements MediaSource { @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - return Util.parseXsDateTime(firstLine); - } catch (ParseException e) { - throw new ParserException(e); - } + return Util.parseXsDateTime(firstLine); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 18e81e53b2..22ba50a3b6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.XmlPullParserUtil; import java.io.IOException; import java.io.InputStream; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -98,13 +97,13 @@ public class DashManifestParser extends DefaultHandler "inputStream does not contain a valid media presentation description"); } return parseMediaPresentationDescription(xpp, uri.toString()); - } catch (XmlPullParserException | ParseException e) { + } catch (XmlPullParserException e) { throw new ParserException(e); } } protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, - String baseUrl) throws XmlPullParserException, IOException, ParseException { + String baseUrl) throws XmlPullParserException, IOException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET); long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); @@ -815,7 +814,7 @@ public class DashManifestParser extends DefaultHandler } protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) - throws ParseException { + throws ParserException { String value = xpp.getAttributeValue(null, name); if (value == null) { return defaultValue; diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 0184018bc9..c41e87d196 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -31,6 +31,7 @@ import android.view.Display; import android.view.WindowManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; @@ -40,7 +41,6 @@ import java.io.InputStream; import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.charset.Charset; -import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -433,11 +433,12 @@ public final class Util { * * @param value The attribute value to decode. * @return The parsed timestamp in milliseconds since the epoch. + * @throws ParserException if an error occurs parsing the dateTime attribute value. */ - public static long parseXsDateTime(String value) throws ParseException { + public static long parseXsDateTime(String value) throws ParserException { Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value); if (!matcher.matches()) { - throw new ParseException("Invalid date/time format: " + value, 0); + throw new ParserException("Invalid date/time format: " + value); } int timezoneShift; From ee8a7f17ff9c81db1cbe665c99eaa9de78f14233 Mon Sep 17 00:00:00 2001 From: vigneshv Date: Wed, 9 Nov 2016 10:06:23 -0800 Subject: [PATCH 079/206] vp9_extension: Fix potential integer overflows ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138652561 --- .../exoplayer2/ext/vp9/VpxDecoder.java | 5 +++- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 30 +++++++++++++++---- extensions/vp9/src/main/jni/vpx_jni.cc | 19 ++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 9af997a58c..0d7547d125 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -122,8 +122,11 @@ import java.nio.ByteBuffer; } outputBuffer.init(inputBuffer.timeUs, outputMode); - if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) { + int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer); + if (getFrameResult == 1) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } else if (getFrameResult == -1) { + return new VpxDecoderException("Buffer initialization failed."); } return null; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index d07b1443fd..c76d0eda03 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -66,28 +66,39 @@ import java.nio.ByteBuffer; /** * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes. + * @return Whether the buffer was resized successfully. */ - public void initForRgbFrame(int width, int height) { + public boolean initForRgbFrame(int width, int height) { this.width = width; this.height = height; this.yuvPlanes = null; - + if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) { + return false; + } int minimumRgbSize = width * height * 2; initData(minimumRgbSize); + return true; } /** * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + * @return Whether the buffer was resized successfully. */ - public void initForYuvFrame(int width, int height, int yStride, int uvStride, + public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { this.width = width; this.height = height; this.colorspace = colorspace; - + int uvHeight = (int) (((long) height + 1) / 2); + if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) { + return false; + } int yLength = yStride * height; - int uvLength = uvStride * ((height + 1) / 2); + int uvLength = uvStride * uvHeight; int minimumYuvSize = yLength + (uvLength * 2); + if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) { + return false; + } initData(minimumYuvSize); if (yuvPlanes == null) { @@ -108,6 +119,7 @@ import java.nio.ByteBuffer; yuvStrides[0] = yStride; yuvStrides[1] = uvStride; yuvStrides[2] = uvStride; + return true; } private void initData(int size) { @@ -119,4 +131,12 @@ import java.nio.ByteBuffer; } } + /** + * Ensures that the result of multiplying individual numbers can fit into the size limit of an + * integer. + */ + private boolean isSafeToMultiply(int a, int b) { + return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); + } + } diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index afaac1c8ae..137ff9ac21 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -83,9 +83,9 @@ DECODER_FUNC(jlong, vpxInit) { const jclass outputBufferClass = env->FindClass( "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", - "(IIIII)V"); + "(IIIII)Z"); initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame", - "(II)V"); + "(II)Z"); dataField = env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); @@ -141,7 +141,11 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { int outputMode = env->GetIntField(jOutputBuffer, outputModeField); if (outputMode == kOutputModeRgb) { // resize buffer if required. - env->CallVoidMethod(jOutputBuffer, initForRgbFrame, img->d_w, img->d_h); + jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame, + img->d_w, img->d_h); + if (initResult == JNI_FALSE) { + return -1; + } // get pointer to the data buffer. const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); @@ -170,9 +174,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } // resize buffer if required. - env->CallVoidMethod(jOutputBuffer, initForYuvFrame, img->d_w, img->d_h, - img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], - colorspace); + jboolean initResult = env->CallBooleanMethod( + jOutputBuffer, initForYuvFrame, img->d_w, img->d_h, + img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], colorspace); + if (initResult == JNI_FALSE) { + return -1; + } // get pointer to the data buffer. const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); From aefc5165fd8363bf62487c37dc4f743d4ed17208 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Nov 2016 14:41:22 -0800 Subject: [PATCH 080/206] Fix cache upgrade ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138687623 --- .../exoplayer2/upstream/cache/SimpleCacheSpanTest.java | 7 ++++++- .../android/exoplayer2/upstream/cache/SimpleCacheSpan.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index a4fbb2af4d..0b40cd7735 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -84,7 +84,12 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { File v1File = createTestFile("asd\u00aa.5.6.v1.exo"); for (File file : cacheDir.listFiles()) { - SimpleCacheSpan.createCacheEntry(file, index); + SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, index); + if (file.equals(wrongEscapedV2file)) { + assertNull(cacheEntry); + } else { + assertNotNull(cacheEntry); + } } assertTrue(v3file.exists()); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 47aefc7820..8c5b7e26e7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -67,6 +67,7 @@ import java.util.regex.Pattern; if (file == null) { return null; } + name = file.getName(); } Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name); From 2620045b0dc0b6777c5cc7651c9430e1d3decf08 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Nov 2016 16:07:55 -0800 Subject: [PATCH 081/206] Add debug logging when codec support checks fail Issue: #2034 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138698239 --- .../android/exoplayer2/ExoPlayerImpl.java | 3 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 55 +++++++++++++++++-- .../exoplayer2/mediacodec/MediaCodecUtil.java | 5 +- .../google/android/exoplayer2/util/Util.java | 6 ++ .../video/MediaCodecVideoRenderer.java | 5 ++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 599f1652f9..ec736ed3a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -73,7 +74,7 @@ import java.util.concurrent.CopyOnWriteArraySet; */ @SuppressLint("HandlerLeak") public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); + Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION + " [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 51c23172a7..a32b4a181f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -21,6 +21,7 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; +import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -32,6 +33,8 @@ import com.google.android.exoplayer2.util.Util; @TargetApi(16) public final class MediaCodecInfo { + public static final String TAG = "MediaCodecInfo"; + /** * The name of the decoder. *

    @@ -111,6 +114,7 @@ public final class MediaCodecInfo { return true; } if (!mimeType.equals(codecMimeType)) { + logNoSupport("codec.mime " + codec + ", " + codecMimeType); return false; } Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec); @@ -124,6 +128,7 @@ public final class MediaCodecInfo { return true; } } + logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); return false; } @@ -139,10 +144,19 @@ public final class MediaCodecInfo { @TargetApi(21) public boolean isVideoSizeSupportedV21(int width, int height) { if (capabilities == null) { + logNoSupport("size.caps"); return false; } VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); - return videoCapabilities != null && videoCapabilities.isSizeSupported(width, height); + if (videoCapabilities == null) { + logNoSupport("size.vCaps"); + return false; + } + if (!videoCapabilities.isSizeSupported(width, height)) { + logNoSupport("size.support, " + width + "x" + height); + return false; + } + return true; } /** @@ -158,11 +172,19 @@ public final class MediaCodecInfo { @TargetApi(21) public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) { if (capabilities == null) { + logNoSupport("sizeAndRate.caps"); return false; } VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); - return videoCapabilities != null && videoCapabilities.areSizeAndRateSupported(width, height, - frameRate); + if (videoCapabilities == null) { + logNoSupport("sizeAndRate.vCaps"); + return false; + } + if (!videoCapabilities.areSizeAndRateSupported(width, height, frameRate)) { + logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); + return false; + } + return true; } /** @@ -176,10 +198,19 @@ public final class MediaCodecInfo { @TargetApi(21) public boolean isAudioSampleRateSupportedV21(int sampleRate) { if (capabilities == null) { + logNoSupport("sampleRate.caps"); return false; } AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); - return audioCapabilities != null && audioCapabilities.isSampleRateSupported(sampleRate); + if (audioCapabilities == null) { + logNoSupport("sampleRate.aCaps"); + return false; + } + if (!audioCapabilities.isSampleRateSupported(sampleRate)) { + logNoSupport("sampleRate.support, " + sampleRate); + return false; + } + return true; } /** @@ -193,10 +224,24 @@ public final class MediaCodecInfo { @TargetApi(21) public boolean isAudioChannelCountSupportedV21(int channelCount) { if (capabilities == null) { + logNoSupport("channelCount.caps"); return false; } AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); - return audioCapabilities != null && audioCapabilities.getMaxInputChannelCount() >= channelCount; + if (audioCapabilities == null) { + logNoSupport("channelCount.aCaps"); + return false; + } + if (audioCapabilities.getMaxInputChannelCount() < channelCount) { + logNoSupport("channelCount.support, " + channelCount); + return false; + } + return true; + } + + private void logNoSupport(String message) { + Log.d(TAG, "FalseCheck [" + message + "] [" + name + ", " + mimeType + "] [" + + Util.DEVICE_DEBUG_INFO + "]"); } private static boolean isAdaptive(CodecCapabilities capabilities) { diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 4836953f8f..3161553209 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -297,8 +297,9 @@ public final class MediaCodecUtil { for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); } - // We assume support for at least 360p. - result = Math.max(result, 480 * 360); + // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are + // the levels mandated by the Android CDD. + result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); } maxH264DecodableFrameSize = result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index c41e87d196..1c25c5da09 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -85,6 +85,12 @@ public final class Util { */ public static final String MODEL = Build.MODEL; + /** + * A concise description of the device that it can be useful to log for debugging purposes. + */ + public static final String DEVICE_DEBUG_INFO = DEVICE + ", " + MODEL + ", " + MANUFACTURER + ", " + + SDK_INT; + private static final String TAG = "Util"; private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index b5a01f0a28..c3921b4d6a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -23,6 +23,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; import android.os.SystemClock; +import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -198,6 +199,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } else { decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); + if (!decoderCapable) { + Log.d(TAG, "FalseCheck [legacyFrameSize, " + format.width + "x" + format.height + "] [" + + Util.DEVICE_DEBUG_INFO + "]"); + } } } From 7ac1cab2d5c5c7b9d32e77555570209eb4b1442a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Nov 2016 07:31:12 -0800 Subject: [PATCH 082/206] Set language to null unless explicitly defined as "und" in container ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138758504 --- library/src/androidTest/assets/ogg/bear.opus.0.dump | 2 +- library/src/androidTest/assets/ogg/bear.opus.1.dump | 2 +- library/src/androidTest/assets/ogg/bear.opus.2.dump | 2 +- library/src/androidTest/assets/ogg/bear.opus.3.dump | 2 +- library/src/androidTest/assets/ogg/bear.opus.unklen.dump | 2 +- .../com/google/android/exoplayer2/extractor/ogg/OpusReader.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/src/androidTest/assets/ogg/bear.opus.0.dump b/library/src/androidTest/assets/ogg/bear.opus.0.dump index 3826692659..8033ce8089 100644 --- a/library/src/androidTest/assets/ogg/bear.opus.0.dump +++ b/library/src/androidTest/assets/ogg/bear.opus.0.dump @@ -22,7 +22,7 @@ track 0: encoderPadding = -1 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 - language = und + language = null drmInitData = - initializationData: data = length 19, hash BFE794DB diff --git a/library/src/androidTest/assets/ogg/bear.opus.1.dump b/library/src/androidTest/assets/ogg/bear.opus.1.dump index f073d36b27..f9aceae68a 100644 --- a/library/src/androidTest/assets/ogg/bear.opus.1.dump +++ b/library/src/androidTest/assets/ogg/bear.opus.1.dump @@ -22,7 +22,7 @@ track 0: encoderPadding = -1 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 - language = und + language = null drmInitData = - initializationData: data = length 19, hash BFE794DB diff --git a/library/src/androidTest/assets/ogg/bear.opus.2.dump b/library/src/androidTest/assets/ogg/bear.opus.2.dump index 6e27201631..f2f07f3e2f 100644 --- a/library/src/androidTest/assets/ogg/bear.opus.2.dump +++ b/library/src/androidTest/assets/ogg/bear.opus.2.dump @@ -22,7 +22,7 @@ track 0: encoderPadding = -1 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 - language = und + language = null drmInitData = - initializationData: data = length 19, hash BFE794DB diff --git a/library/src/androidTest/assets/ogg/bear.opus.3.dump b/library/src/androidTest/assets/ogg/bear.opus.3.dump index 8d4f451698..905055797c 100644 --- a/library/src/androidTest/assets/ogg/bear.opus.3.dump +++ b/library/src/androidTest/assets/ogg/bear.opus.3.dump @@ -22,7 +22,7 @@ track 0: encoderPadding = -1 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 - language = und + language = null drmInitData = - initializationData: data = length 19, hash BFE794DB diff --git a/library/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/src/androidTest/assets/ogg/bear.opus.unklen.dump index 070c9ef8a6..cd29da3e27 100644 --- a/library/src/androidTest/assets/ogg/bear.opus.unklen.dump +++ b/library/src/androidTest/assets/ogg/bear.opus.unklen.dump @@ -22,7 +22,7 @@ track 0: encoderPadding = -1 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 - language = und + language = null drmInitData = - initializationData: data = length 19, hash BFE794DB diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index 108743c764..8ed8a4a01d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -81,7 +81,7 @@ import java.util.List; setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, - "und"); + null); headerRead = true; } else { boolean headerPacket = packet.readInt() == OPUS_CODE; From 8cc7dfda7dfae000e41aeb50840786547241ecf0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Nov 2016 07:34:29 -0800 Subject: [PATCH 083/206] Fix threading issues between ExoPlayerImpl/ExoPlayerImplInternal ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138758739 --- .../google/android/exoplayer2/ExoPlayer.java | 34 +++-- .../android/exoplayer2/ExoPlayerImpl.java | 14 +- .../exoplayer2/ExoPlayerImplInternal.java | 131 +++++++++++------- 3 files changed, 112 insertions(+), 67 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 31efdd82b1..83e4fd7e30 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -112,6 +112,19 @@ public interface ExoPlayer { */ interface EventListener { + /** + * Called when the timeline and/or manifest has been refreshed. + *

    + * Note that if the timeline has changed then a position discontinuity may also have occurred. + * For example the current period index may have changed as a result of periods being added or + * removed from the timeline. The will not be reported via a separate call to + * {@link #onPositionDiscontinuity()}. + * + * @param timeline The latest timeline, or null if the timeline is being cleared. + * @param manifest The latest manifest, or null if the manifest is being cleared. + */ + void onTimelineChanged(Timeline timeline, Object manifest); + /** * Called when the available or selected tracks change. * @@ -138,14 +151,6 @@ public interface ExoPlayer { */ void onPlayerStateChanged(boolean playWhenReady, int playbackState); - /** - * Called when timeline and/or manifest has been refreshed. - * - * @param timeline The latest timeline, or null if the timeline is being cleared. - * @param manifest The latest manifest, or null if the manifest is being cleared. - */ - void onTimelineChanged(Timeline timeline, Object manifest); - /** * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} * immediately after this method is called. The player instance can still be used, and @@ -156,9 +161,14 @@ public interface ExoPlayer { void onPlayerError(ExoPlaybackException error); /** - * Called when a position discontinuity occurs. Position discontinuities occur when seeks are - * performed, when playbacks transition from one period in the timeline to the next, and when - * the player introduces discontinuities internally. + * Called when a position discontinuity occurs without a change to the timeline. A position + * discontinuity occurs when the current window or period index changes (as a result of playback + * transitioning from one period in the timeline to the next), or when the playback position + * jumps within the period currently being played (as a result of a seek being performed, or + * when the source introduces a discontinuity internally). + *

    + * When a position discontinuity occurs as a result of a change to the timeline this method is + * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. */ void onPositionDiscontinuity(); @@ -403,7 +413,7 @@ public interface ExoPlayer { Timeline getCurrentTimeline(); /** - * Returns the index of the period currently being played, or {@link C#INDEX_UNSET} if unknown. + * Returns the index of the period currently being played. */ int getCurrentPeriodIndex(); diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ec736ed3a0..5bf1b599e2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -20,8 +20,8 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; +import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -329,8 +329,7 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { - pendingSeekAcks -= msg.arg1; - if (pendingSeekAcks == 0) { + if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -348,10 +347,11 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - @SuppressWarnings("unchecked") - Pair timelineAndManifest = (Pair) msg.obj; - timeline = timelineAndManifest.first; - manifest = timelineAndManifest.second; + SourceInfo sourceInfo = (SourceInfo) msg.obj; + timeline = sourceInfo.timeline; + manifest = sourceInfo.manifest; + playbackInfo = sourceInfo.playbackInfo; + pendingSeekAcks -= sourceInfo.seekAcks; for (EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 35f5f393be..743015509b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -79,6 +79,22 @@ import java.io.IOException; } + public static final class SourceInfo { + + public final Timeline timeline; + public final Object manifest; + public final PlaybackInfo playbackInfo; + public final int seekAcks; + + public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) { + this.timeline = timeline; + this.manifest = manifest; + this.playbackInfo = playbackInfo; + this.seekAcks = seekAcks; + } + + } + private static final String TAG = "ExoPlayerImplInternal"; // External messages @@ -533,15 +549,14 @@ import java.io.IOException; try { if (periodIndex == playbackInfo.periodIndex - && ((periodPositionUs == C.TIME_UNSET && playbackInfo.positionUs == C.TIME_UNSET) - || ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000)))) { + && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { // Seek position equals the current position. Do nothing. return; } periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); } } @@ -551,11 +566,10 @@ import java.io.IOException; rebuffering = false; setState(ExoPlayer.STATE_BUFFERING); - if (periodPositionUs == C.TIME_UNSET || (readingPeriodHolder != playingPeriodHolder - && (periodIndex == playingPeriodHolder.index - || periodIndex == readingPeriodHolder.index))) { - // Clear the timeline because either the seek position is not known, or a renderer is reading - // ahead to the next period and the seek is to either the playing or reading period. + if (readingPeriodHolder != playingPeriodHolder && (periodIndex == playingPeriodHolder.index + || periodIndex == readingPeriodHolder.index)) { + // Clear the timeline because a renderer is reading ahead to the next period and the seek is + // to either the playing or reading period. periodIndex = C.INDEX_UNSET; } @@ -605,9 +619,7 @@ import java.io.IOException; playingPeriodHolder = null; readingPeriodHolder = null; loadingPeriodHolder = null; - if (periodPositionUs != C.TIME_UNSET) { - resetRendererPosition(periodPositionUs); - } + resetRendererPosition(periodPositionUs); } updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); @@ -821,30 +833,37 @@ import java.io.IOException; private void handleSourceInfoRefreshed(Pair timelineAndManifest) throws ExoPlaybackException, IOException { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, timelineAndManifest).sendToTarget(); - Timeline oldTimeline = this.timeline; - this.timeline = timelineAndManifest.first; + Timeline oldTimeline = timeline; + timeline = timelineAndManifest.first; + Object manifest = timelineAndManifest.second; - if (pendingInitialSeekCount > 0) { - Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - if (periodPosition == null) { - // TODO: We should probably propagate an error here. - // We failed to resolve the seek position. Stop the player. - stopInternal(); - return; + if (oldTimeline == null) { + if (pendingInitialSeekCount > 0) { + Pair periodPosition = resolveSeekPosition(pendingSeekPosition); + if (periodPosition == null) { + // TODO: We should probably propagate an error here. + // We failed to resolve the seek position. Stop the player. + finishSourceInfoRefresh(manifest, false); + stopInternal(); + return; + } + playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); + } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { + Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); } - playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); - eventHandler.obtainMessage(MSG_SEEK_ACK, pendingInitialSeekCount, 0, playbackInfo) - .sendToTarget(); - pendingInitialSeekCount = 0; - pendingSeekPosition = null; } // Update the loaded periods to take into account the new timeline. if (playingPeriodHolder != null) { int index = timeline.getIndexOfPeriod(playingPeriodHolder.uid); if (index == C.INDEX_UNSET) { - attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); + boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); + finishSourceInfoRefresh(manifest, true); + if (!restarted) { + // TODO: We should probably propagate an error here. + stopInternal(); + } return; } @@ -873,8 +892,8 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(index, playbackInfo.positionUs); if (newPositionUs != playbackInfo.positionUs) { playbackInfo = new PlaybackInfo(index, newPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } + finishSourceInfoRefresh(manifest, true); return; } @@ -899,7 +918,12 @@ import java.io.IOException; Object uid = loadingPeriodHolder.uid; int index = timeline.getIndexOfPeriod(uid); if (index == C.INDEX_UNSET) { - attemptRestart(loadingPeriodHolder.index, oldTimeline, timeline); + boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); + finishSourceInfoRefresh(manifest, true); + if (!restarted) { + // TODO: We should probably propagate an error here. + stopInternal(); + } return; } else { int windowIndex = timeline.getPeriod(index, this.period).windowIndex; @@ -908,7 +932,6 @@ import java.io.IOException; } } - // TODO[playlists]: Signal the identifier discontinuity, even if the index hasn't changed. if (oldTimeline != null) { int newPlayingIndex = playingPeriodHolder != null ? playingPeriodHolder.index : loadingPeriodHolder != null ? loadingPeriodHolder.index : C.INDEX_UNSET; @@ -916,18 +939,16 @@ import java.io.IOException; && newPlayingIndex != playbackInfo.periodIndex) { playbackInfo = new PlaybackInfo(newPlayingIndex, playbackInfo.positionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } } + finishSourceInfoRefresh(manifest, true); } - private void attemptRestart(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { + private boolean attemptRestart(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { int newPeriodIndex = resolveSubsequentPeriod(oldPeriodIndex, oldTimeline, newTimeline); if (newPeriodIndex == C.INDEX_UNSET) { - // TODO: We should probably propagate an error here. // We failed to find a replacement period. Stop the player. - stopInternal(); - return; + return false; } // Release all loaded periods. @@ -943,9 +964,18 @@ import java.io.IOException; timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); newPeriodIndex = defaultPosition.first; long newPlayingPositionUs = defaultPosition.second; - playbackInfo = new PlaybackInfo(newPeriodIndex, newPlayingPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + return true; + } + + private void finishSourceInfoRefresh(Object manifest, boolean processedInitialSeeks) { + SourceInfo sourceInfo = new SourceInfo(timeline, manifest, playbackInfo, + processedInitialSeeks ? pendingInitialSeekCount : 0); + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, sourceInfo).sendToTarget(); + if (processedInitialSeeks) { + pendingInitialSeekCount = 0; + pendingSeekPosition = null; + } } /** @@ -1076,17 +1106,22 @@ import java.io.IOException; int windowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; boolean isFirstPeriodInWindow = newLoadingPeriodIndex == timeline.getWindow(windowIndex, window).firstPeriodIndex; - long periodStartPositionUs = loadingPeriodHolder == null ? playbackInfo.positionUs - : (isFirstPeriodInWindow ? C.TIME_UNSET : 0); - if (periodStartPositionUs == C.TIME_UNSET) { - // This is the first period of a new window or we don't have a start position, so seek to - // the default position for the window. If we're buffering ahead we also project the - // default position so that it's correct for starting playing the buffered duration of - // time in the future. - long defaultPositionProjectionUs = loadingPeriodHolder == null ? 0 - : (loadingPeriodHolder.rendererPositionOffsetUs - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - loadingPeriodHolder.startPositionUs - rendererPositionUs); + long periodStartPositionUs; + if (loadingPeriodHolder == null) { + periodStartPositionUs = playbackInfo.startPositionUs; + } else if (!isFirstPeriodInWindow) { + // We're starting to buffer a new period in the current window. Always start from the + // beginning of the period. + periodStartPositionUs = 0; + } else { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position. The expected delay until playback + // transitions is equal the duration of media that's currently buffered (assuming no + // interruptions). Hence we project the default start position forward by the duration of + // the buffer, and start buffering from this point. + long defaultPositionProjectionUs = loadingPeriodHolder.rendererPositionOffsetUs + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs - rendererPositionUs; Pair defaultPosition = getPeriodPosition(timeline, windowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); if (defaultPosition == null) { From 16ddc84d937100d991b05461c03165275bd945af Mon Sep 17 00:00:00 2001 From: cblay Date: Thu, 10 Nov 2016 07:39:58 -0800 Subject: [PATCH 084/206] Make PriorityTaskManager constructor public. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138759164 --- .../com/google/android/exoplayer2/util/PriorityTaskManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java b/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java index cc6a17913b..fb61d3ba4a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java @@ -46,7 +46,7 @@ public final class PriorityTaskManager { private final PriorityQueue queue; private int highestPriority; - private PriorityTaskManager() { + public PriorityTaskManager() { queue = new PriorityQueue<>(10, Collections.reverseOrder()); highestPriority = Integer.MIN_VALUE; } From 92a98d1ce2a27266fa69927235929f45678aac40 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 10 Nov 2016 10:54:13 -0800 Subject: [PATCH 085/206] Encrypt SimpleCache index file. Clean up AtomicFile and make it return a custom FileOutputStream for writing which handles IOException automatically during write operations. It also syncs the file descriptor and deletes the backup file on close() call. This fixes the order of flush and close operations when the fileoutputstream is wrapped by another OutputStream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138779187 --- .../cache/CachedContentIndexTest.java | 80 ++++- .../exoplayer2/util/AtomicFileTest.java | 85 ++++++ .../exoplayer2/upstream/cache/Cache.java | 28 +- .../upstream/cache/CacheDataSink.java | 9 +- .../upstream/cache/CacheDataSource.java | 6 +- .../upstream/cache/CachedContent.java | 9 +- .../upstream/cache/CachedContentIndex.java | 106 +++++-- .../cache/LeastRecentlyUsedCacheEvictor.java | 7 +- .../upstream/cache/SimpleCache.java | 49 +++- .../android/exoplayer2/util/AtomicFile.java | 276 +++++++++--------- 10 files changed, 443 insertions(+), 212 deletions(-) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 4666c81dfb..ad64de50ca 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -8,9 +8,11 @@ import com.google.android.exoplayer2.testutil.TestUtil; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Set; +import junit.framework.AssertionFailedError; /** * Tests {@link CachedContentIndex}. @@ -91,21 +93,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { } public void testStoreAndLoad() throws Exception { - index.addNew(new CachedContent(5, "key1", 10)); - index.add("key2"); - - index.store(); - - CachedContentIndex index2 = new CachedContentIndex(cacheDir); - index2.load(); - - Set keys = index.getKeys(); - Set keys2 = index2.getKeys(); - assertEquals(keys, keys2); - for (String key : keys) { - assertEquals(index.getContentLength(key), index2.getContentLength(key)); - assertEquals(index.get(key).getSpans(), index2.get(key).getSpans()); - } + assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); } public void testLoadV1() throws Exception { @@ -168,4 +156,66 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertEquals(1, CachedContentIndex.getNewId(idToKey)); } + public void testEncryption() throws Exception { + byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key + byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key + + assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), + new CachedContentIndex(cacheDir, key)); + + // Rename the index file from the test above + File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME); + File file2 = new File(cacheDir, "file2compare"); + assertTrue(file1.renameTo(file2)); + + // Write a new index file + assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), + new CachedContentIndex(cacheDir, key)); + + assertEquals(file2.length(), file1.length()); + // Assert file content is different + FileInputStream fis1 = new FileInputStream(file1); + FileInputStream fis2 = new FileInputStream(file2); + for (int b; (b = fis1.read()) == fis2.read();) { + assertTrue(b != -1); + } + + boolean threw = false; + try { + assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), + new CachedContentIndex(cacheDir, key2)); + } catch (AssertionFailedError e) { + threw = true; + } + assertTrue("Encrypted index file can not be read with different encryption key", threw); + + try { + assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), + new CachedContentIndex(cacheDir)); + } catch (AssertionFailedError e) { + threw = true; + } + assertTrue("Encrypted index file can not be read without encryption key", threw); + + // Non encrypted index file can be read even when encryption key provided. + assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir), + new CachedContentIndex(cacheDir, key)); + } + + private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2) + throws IOException { + index.addNew(new CachedContent(5, "key1", 10)); + index.add("key2"); + index.store(); + + index2.load(); + Set keys = index.getKeys(); + Set keys2 = index2.getKeys(); + assertEquals(keys, keys2); + for (String key : keys) { + assertEquals(index.getContentLength(key), index2.getContentLength(key)); + assertEquals(index.get(key).getSpans(), index2.get(key).getSpans()); + } + } + } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java new file mode 100644 index 0000000000..afe28a1e99 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 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.util; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Tests {@link AtomicFile}. + */ +public class AtomicFileTest extends InstrumentationTestCase { + + private File tempFolder; + private File file; + private AtomicFile atomicFile; + + @Override + public void setUp() throws Exception { + tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext()); + file = new File(tempFolder, "atomicFile"); + atomicFile = new AtomicFile(file); + } + + @Override + protected void tearDown() throws Exception { + TestUtil.recursiveDelete(tempFolder); + } + + public void testDelete() throws Exception { + assertTrue(file.createNewFile()); + atomicFile.delete(); + assertFalse(file.exists()); + } + + public void testWriteEndRead() throws Exception { + OutputStream output = atomicFile.startWrite(); + output.write(5); + atomicFile.endWrite(output); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(5); + output.write(6); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(6); + + assertRead(); + + output = atomicFile.startWrite(); + + assertRead(); + } + + private void assertRead() throws IOException { + InputStream input = atomicFile.openRead(); + assertEquals(5, input.read()); + assertEquals(-1, input.read()); + input.close(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 27b989c36f..8dcfe75670 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import java.io.File; +import java.io.IOException; import java.util.NavigableSet; import java.util.Set; @@ -60,6 +61,21 @@ public interface Cache { void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); } + + /** + * Thrown when an error is encountered when writing data. + */ + class CacheException extends IOException { + + public CacheException(String message) { + super(message); + } + + public CacheException(IOException cause) { + super(cause); + } + + } /** * Registers a listener to listen for changes to a given key. @@ -125,7 +141,7 @@ public interface Cache { * @return The {@link CacheSpan}. * @throws InterruptedException */ - CacheSpan startReadWrite(String key, long position) throws InterruptedException; + CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; /** * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then @@ -135,7 +151,7 @@ public interface Cache { * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. */ - CacheSpan startReadWriteNonBlocking(String key, long position); + CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; /** * Obtains a cache file into which data can be written. Must only be called when holding a @@ -147,7 +163,7 @@ public interface Cache { * is enough space in the cache. * @return The file into which data should be written. */ - File startFile(String key, long position, long maxLength); + File startFile(String key, long position, long maxLength) throws CacheException; /** * Commits a file into the cache. Must only be called when holding a corresponding hole @@ -155,7 +171,7 @@ public interface Cache { * * @param file A newly written cache file. */ - void commitFile(File file); + void commitFile(File file) throws CacheException; /** * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which @@ -170,7 +186,7 @@ public interface Cache { * * @param span The {@link CacheSpan} to remove. */ - void removeSpan(CacheSpan span); + void removeSpan(CacheSpan span) throws CacheException; /** * Queries if a range is entirely available in the cache. @@ -188,7 +204,7 @@ public interface Cache { * @param key The cache key for the data. * @param length The length of the data. */ - void setContentLength(String key, long length); + void setContentLength(String key, long length) throws CacheException; /** * Returns the content length for the given key if one set, or {@link diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 96c198b4c9..6a301f8a2e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -18,10 +18,10 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -42,7 +42,7 @@ public final class CacheDataSink implements DataSink { /** * Thrown when IOException is encountered when writing data into sink. */ - public static class CacheDataSinkException extends IOException { + public static class CacheDataSinkException extends CacheException { public CacheDataSinkException(IOException cause) { super(cause); @@ -50,7 +50,6 @@ public final class CacheDataSink implements DataSink { } - /** * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for @@ -71,7 +70,7 @@ public final class CacheDataSink implements DataSink { dataSpecBytesWritten = 0; try { openNextOutputStream(); - } catch (FileNotFoundException e) { + } catch (IOException e) { throw new CacheDataSinkException(e); } } @@ -112,7 +111,7 @@ public final class CacheDataSink implements DataSink { } } - private void openNextOutputStream() throws FileNotFoundException { + private void openNextOutputStream() throws IOException { file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); outputStream = new FileOutputStream(file); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index d53a5d8fe8..b98eadc4cc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; @@ -328,7 +328,7 @@ public final class CacheDataSource implements DataSource { return successful; } - private void setContentLength(long length) { + private void setContentLength(long length) throws IOException { cache.setContentLength(key, length); } @@ -349,7 +349,7 @@ public final class CacheDataSource implements DataSource { } private void handleBeforeThrow(IOException exception) { - if (currentDataSource == cacheReadDataSource || exception instanceof CacheDataSinkException) { + if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) { seenCacheError = true; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index a25688f9db..c744a176ad 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -150,14 +151,18 @@ import java.util.TreeSet; * * @param cacheSpan Span to be copied and updated. * @return a span with the updated last access time. + * @throws CacheException If renaming of the underlying span file failed. */ - public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) { + public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException { // Remove the old span from the in-memory representation. Assertions.checkState(cachedSpans.remove(cacheSpan)); // Obtain a new span with updated last access timestamp. SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); // Rename the cache file - cacheSpan.file.renameTo(newCacheSpan.file); + if (!cacheSpan.file.renameTo(newCacheSpan.file)) { + throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file + + " failed."); + } // Add the updated span back into the in-memory representation. cachedSpans.add(newCacheSpan); return newCacheSpan; diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 4f884606ee..47019b98a3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -17,18 +17,30 @@ package com.google.android.exoplayer2.upstream.cache; import android.util.SparseArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.Util; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; +import java.util.Random; import java.util.Set; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; /** * This class maintains the index of cached content. @@ -36,15 +48,36 @@ import java.util.Set; /*package*/ final class CachedContentIndex { public static final String FILE_NAME = "cached_content_index.exi"; + private static final int VERSION = 1; + private static final int FLAG_ENCRYPTED_INDEX = 1; + private final HashMap keyToContent; private final SparseArray idToKey; private final AtomicFile atomicFile; + private final Cipher cipher; + private final SecretKeySpec secretKeySpec; private boolean changed; /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ public CachedContentIndex(File cacheDir) { + this(cacheDir, null); + } + + /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ + public CachedContentIndex(File cacheDir, byte[] secretKey) { + if (secretKey != null) { + try { + cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + secretKeySpec = new SecretKeySpec(secretKey, "AES"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); // Should never happen. + } + } else { + cipher = null; + secretKeySpec = null; + } keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); @@ -53,18 +86,15 @@ import java.util.Set; /** Loads the index file. */ public void load() { Assertions.checkState(!changed); - File cacheIndex = atomicFile.getBaseFile(); - if (cacheIndex.exists()) { - if (!readFile()) { - cacheIndex.delete(); - keyToContent.clear(); - idToKey.clear(); - } + if (!readFile()) { + atomicFile.delete(); + keyToContent.clear(); + idToKey.clear(); } } /** Stores the index data to index file if there is a change. */ - public void store() { + public void store() throws CacheException { if (!changed) { return; } @@ -177,13 +207,30 @@ import java.util.Set; private boolean readFile() { DataInputStream input = null; try { - input = new DataInputStream(atomicFile.openRead()); + InputStream inputStream = atomicFile.openRead(); + input = new DataInputStream(inputStream); int version = input.readInt(); if (version != VERSION) { // Currently there is no other version return false; } - input.readInt(); // ignore flags placeholder + + int flags = input.readInt(); + if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { + if (cipher == null) { + return false; + } + byte[] initializationVector = new byte[16]; + input.read(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); + } + input = new DataInputStream(new CipherInputStream(inputStream, cipher)); + } + int count = input.readInt(); int hashCode = 0; for (int i = 0; i < count; i++) { @@ -204,14 +251,30 @@ import java.util.Set; return true; } - private void writeFile() { - FileOutputStream outputStream = null; + private void writeFile() throws CacheException { + DataOutputStream output = null; try { - outputStream = atomicFile.startWrite(); - DataOutputStream output = new DataOutputStream(outputStream); - + OutputStream outputStream = atomicFile.startWrite(); + output = new DataOutputStream(outputStream); output.writeInt(VERSION); - output.writeInt(0); // flags placeholder + + int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; + output.writeInt(flags); + + if (cipher != null) { + byte[] initializationVector = new byte[16]; + new Random().nextBytes(initializationVector); + output.write(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); // Should never happen. + } + output.flush(); + output = new DataOutputStream(new CipherOutputStream(outputStream, cipher)); + } + output.writeInt(keyToContent.size()); int hashCode = 0; for (CachedContent cachedContent : keyToContent.values()) { @@ -219,12 +282,11 @@ import java.util.Set; hashCode += cachedContent.headerHashCode(); } output.writeInt(hashCode); - - output.flush(); - atomicFile.finishWrite(outputStream); + atomicFile.endWrite(output); } catch (IOException e) { - atomicFile.failWrite(outputStream); - throw new RuntimeException("Writing the new cache index file failed.", e); + throw new CacheException(e); + } finally { + Util.closeQuietly(output); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index 791fb677f1..d2a84f65f4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import java.util.Comparator; import java.util.TreeSet; @@ -74,7 +75,11 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar private void evictCache(Cache cache, long requiredSpace) { while (currentSize + requiredSpace > maxBytes) { - cache.removeSpan(leastRecentlyUsed.first()); + try { + cache.removeSpan(leastRecentlyUsed.first()); + } catch (CacheException e) { + // do nothing. + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 53a44a5797..ad3569aca0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -38,18 +38,33 @@ public final class SimpleCache implements Cache { private final CachedContentIndex index; private final HashMap> listeners; private long totalSpace = 0; + private CacheException initializationException; /** * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence * the directory cannot be used to store other files. * * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. */ public SimpleCache(File cacheDir, CacheEvictor evictor) { + this(cacheDir, evictor, null); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir); + this.index = new CachedContentIndex(cacheDir, secretKey); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -58,7 +73,11 @@ public final class SimpleCache implements Cache { public void run() { synchronized (SimpleCache.this) { conditionVariable.open(); - initialize(); + try { + initialize(); + } catch (CacheException e) { + initializationException = e; + } SimpleCache.this.evictor.onCacheInitialized(); } } @@ -106,7 +125,7 @@ public final class SimpleCache implements Cache { @Override public synchronized SimpleCacheSpan startReadWrite(String key, long position) - throws InterruptedException { + throws InterruptedException, CacheException { while (true) { SimpleCacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { @@ -122,7 +141,12 @@ public final class SimpleCache implements Cache { } @Override - public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) { + public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) + throws CacheException { + if (initializationException != null) { + throw initializationException; + } + SimpleCacheSpan cacheSpan = getSpan(key, position); // Read case. @@ -144,7 +168,8 @@ public final class SimpleCache implements Cache { } @Override - public synchronized File startFile(String key, long position, long maxLength) { + public synchronized File startFile(String key, long position, long maxLength) + throws CacheException { Assertions.checkState(lockedSpans.containsKey(key)); if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. @@ -157,7 +182,7 @@ public final class SimpleCache implements Cache { } @Override - public synchronized void commitFile(File file) { + public synchronized void commitFile(File file) throws CacheException { SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); Assertions.checkState(span != null); Assertions.checkState(lockedSpans.containsKey(span.key)); @@ -199,7 +224,7 @@ public final class SimpleCache implements Cache { * @param position The position of the span being requested. * @return The corresponding cache {@link SimpleCacheSpan}. */ - private SimpleCacheSpan getSpan(String key, long position) { + private SimpleCacheSpan getSpan(String key, long position) throws CacheException { CachedContent cachedContent = index.get(key); if (cachedContent == null) { return SimpleCacheSpan.createOpenHole(key, position); @@ -219,7 +244,7 @@ public final class SimpleCache implements Cache { /** * Ensures that the cache's in-memory representation has been initialized. */ - private void initialize() { + private void initialize() throws CacheException { if (!cacheDir.exists()) { cacheDir.mkdirs(); return; @@ -259,7 +284,7 @@ public final class SimpleCache implements Cache { notifySpanAdded(span); } - private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) { + private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { CachedContent cachedContent = index.get(span.key); Assertions.checkState(cachedContent.removeSpan(span)); totalSpace -= span.length; @@ -271,7 +296,7 @@ public final class SimpleCache implements Cache { } @Override - public synchronized void removeSpan(CacheSpan span) { + public synchronized void removeSpan(CacheSpan span) throws CacheException { removeSpan(span, true); } @@ -279,7 +304,7 @@ public final class SimpleCache implements Cache { * Scans all of the cached spans in the in-memory representation, removing any for which files * no longer exist. */ - private void removeStaleSpansAndCachedContents() { + private void removeStaleSpansAndCachedContents() throws CacheException { LinkedList spansToBeRemoved = new LinkedList<>(); for (CachedContent cachedContent : index.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { @@ -336,7 +361,7 @@ public final class SimpleCache implements Cache { } @Override - public synchronized void setContentLength(String key, long length) { + public synchronized void setContentLength(String key, long length) throws CacheException { index.setContentLength(key, length); index.store(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 3746a741e0..10a473f177 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2016 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. @@ -22,192 +22,176 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; /** - * Exoplayer internal version of the framework's {@link android.util.AtomicFile}, - * a helper class for performing atomic operations on a file by creating a - * backup file until a write has successfully completed. - *

    - * Atomic file guarantees file integrity by ensuring that a file has - * been completely written and sync'd to disk before removing its backup. - * As long as the backup file exists, the original file is considered - * to be invalid (left over from a previous attempt to write the file). - *

    - * Atomic file does not confer any file locking semantics. - * Do not use this class when the file may be accessed or modified concurrently - * by multiple threads or processes. The caller is responsible for ensuring - * appropriate mutual exclusion invariants whenever it accesses the file. - *

    + * A helper class for performing atomic operations on a file by creating a backup file until a write + * has successfully completed. + * + *

    Atomic file guarantees file integrity by ensuring that a file has been completely written and + * sync'd to disk before removing its backup. As long as the backup file exists, the original file + * is considered to be invalid (left over from a previous attempt to write the file). + * + *

    Atomic file does not confer any file locking semantics. Do not use this class when the file + * may be accessed or modified concurrently by multiple threads or processes. The caller is + * responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file. */ -public class AtomicFile { - private final File mBaseName; - private final File mBackupName; +public final class AtomicFile { + + private static final String TAG = "AtomicFile"; + + private final File baseName; + private final File backupName; /** - * Create a new AtomicFile for a file located at the given File path. - * The secondary backup file will be the same file path with ".bak" appended. + * Create a new AtomicFile for a file located at the given File path. The secondary backup file + * will be the same file path with ".bak" appended. */ public AtomicFile(File baseName) { - mBaseName = baseName; - mBackupName = new File(baseName.getPath() + ".bak"); + this.baseName = baseName; + backupName = new File(baseName.getPath() + ".bak"); } - /** - * Return the path to the base file. You should not generally use this, - * as the data at that path may not be valid. - */ - public File getBaseFile() { - return mBaseName; - } - - /** - * Delete the atomic file. This deletes both the base and backup files. - */ + /** Delete the atomic file. This deletes both the base and backup files. */ public void delete() { - mBaseName.delete(); - mBackupName.delete(); + baseName.delete(); + backupName.delete(); } /** - * Start a new write operation on the file. This returns a FileOutputStream - * to which you can write the new file data. The existing file is replaced - * with the new data. You must not directly close the given - * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} - * or {@link #failWrite(FileOutputStream)}. + * Start a new write operation on the file. This returns an {@link OutputStream} to which you can + * write the new file data. If the whole data is written successfully you must call + * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()} + * only to free up resources used by it. * - *

    Note that if another thread is currently performing - * a write, this will simply replace whatever that thread is writing - * with the new file being written by this thread, and when the other - * thread finishes the write the new write operation will no longer be - * safe (or will be lost). You must do your own threading protection for - * access to AtomicFile. + *

    Example usage: + * + *

    +   *   DataOutputStream dataOutput = null;
    +   *   try {
    +   *     OutputStream outputStream = atomicFile.startWrite();
    +   *     dataOutput = new DataOutputStream(outputStream); // Wrapper stream
    +   *     dataOutput.write(data1);
    +   *     dataOutput.write(data2);
    +   *     atomicFile.endWrite(dataOutput); // Pass wrapper stream
    +   *   } finally{
    +   *     if (dataOutput != null) {
    +   *       dataOutput.close();
    +   *     }
    +   *   }
    +   * 
    + * + *

    Note that if another thread is currently performing a write, this will simply replace + * whatever that thread is writing with the new file being written by this thread, and when the + * other thread finishes the write the new write operation will no longer be safe (or will be + * lost). You must do your own threading protection for access to AtomicFile. */ - public FileOutputStream startWrite() throws IOException { + public OutputStream startWrite() throws IOException { // Rename the current file so it may be used as a backup during the next read - if (mBaseName.exists()) { - if (!mBackupName.exists()) { - if (!mBaseName.renameTo(mBackupName)) { - Log.w("AtomicFile", "Couldn't rename file " + mBaseName - + " to backup file " + mBackupName); + if (baseName.exists()) { + if (!backupName.exists()) { + if (!baseName.renameTo(backupName)) { + Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName); } } else { - mBaseName.delete(); + baseName.delete(); } } - FileOutputStream str = null; + OutputStream str = null; try { - str = new FileOutputStream(mBaseName); + str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e) { - File parent = mBaseName.getParentFile(); + File parent = baseName.getParentFile(); if (!parent.mkdirs()) { - throw new IOException("Couldn't create directory " + mBaseName); + throw new IOException("Couldn't create directory " + baseName); } try { - str = new FileOutputStream(mBaseName); + str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e2) { - throw new IOException("Couldn't create " + mBaseName); + throw new IOException("Couldn't create " + baseName); } } return str; } /** - * Call when you have successfully finished writing to the stream - * returned by {@link #startWrite()}. This will close, sync, and - * commit the new data. The next attempt to read the atomic file - * will return the new file stream. - */ - public void finishWrite(FileOutputStream str) { - if (str != null) { - sync(str); - try { - str.close(); - mBackupName.delete(); - } catch (IOException e) { - Log.w("AtomicFile", "finishWrite: Got exception:", e); - } - } - } - - /** - * Call when you have failed for some reason at writing to the stream - * returned by {@link #startWrite()}. This will close the current - * write stream, and roll back to the previous state of the file. - */ - public void failWrite(FileOutputStream str) { - if (str != null) { - sync(str); - try { - str.close(); - mBaseName.delete(); - mBackupName.renameTo(mBaseName); - } catch (IOException e) { - Log.w("AtomicFile", "failWrite: Got exception:", e); - } - } - } - - /** - * Open the atomic file for reading. If there previously was an - * incomplete write, this will roll back to the last good data before - * opening for read. You should call close() on the FileInputStream when - * you are done reading from it. + * Call when you have successfully finished writing to the stream returned by {@link + * #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the + * atomic file will return the new file stream. * - *

    Note that if another thread is currently performing - * a write, this will incorrectly consider it to be in the state of a bad - * write and roll back, causing the new data currently being written to - * be dropped. You must do your own threading protection for access to - * AtomicFile. + * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link + * #startWrite()}. + * @see #startWrite() */ - public FileInputStream openRead() throws FileNotFoundException { - if (mBackupName.exists()) { - mBaseName.delete(); - mBackupName.renameTo(mBaseName); - } - return new FileInputStream(mBaseName); + public void endWrite(OutputStream str) throws IOException { + str.close(); + // If close() throws exception, the next line is skipped. + backupName.delete(); } /** - * A convenience for {@link #openRead()} that also reads all of the - * file contents into a byte array which is returned. + * Open the atomic file for reading. If there previously was an incomplete write, this will roll + * back to the last good data before opening for read. + * + *

    Note that if another thread is currently performing a write, this will incorrectly consider + * it to be in the state of a bad write and roll back, causing the new data currently being + * written to be dropped. You must do your own threading protection for access to AtomicFile. */ - public byte[] readFully() throws IOException { - FileInputStream stream = openRead(); - try { - int pos = 0; - int avail = stream.available(); - byte[] data = new byte[avail]; - while (true) { - int amt = stream.read(data, pos, data.length - pos); - //Log.i("foo", "Read " + amt + " bytes at " + pos - // + " of avail " + data.length); - if (amt <= 0) { - //Log.i("foo", "**** FINISHED READING: pos=" + pos - // + " len=" + data.length); - return data; - } - pos += amt; - avail = stream.available(); - if (avail > data.length - pos) { - byte[] newData = new byte[pos + avail]; - System.arraycopy(data, 0, newData, 0, pos); - data = newData; - } - } - } finally { - stream.close(); + public InputStream openRead() throws FileNotFoundException { + restoreBackup(); + return new FileInputStream(baseName); + } + + private void restoreBackup() { + if (backupName.exists()) { + baseName.delete(); + backupName.renameTo(baseName); } } - private static boolean sync(FileOutputStream stream) { - try { - if (stream != null) { - stream.getFD().sync(); - } - return true; - } catch (IOException e) { - // do nothing + private static final class AtomicFileOutputStream extends OutputStream { + + private final FileOutputStream fileOutputStream; + private boolean closed = false; + + public AtomicFileOutputStream(File file) throws FileNotFoundException { + fileOutputStream = new FileOutputStream(file); + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + flush(); + try { + fileOutputStream.getFD().sync(); + } catch (IOException e) { + Log.w(TAG, "Failed to sync file descriptor:", e); + } + fileOutputStream.close(); + } + + @Override + public void flush() throws IOException { + fileOutputStream.flush(); + } + + @Override + public void write(int b) throws IOException { + fileOutputStream.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + fileOutputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + fileOutputStream.write(b, off, len); } - return false; } } From ff1a008817db3e3bab93cf78f1b17dcaaf540688 Mon Sep 17 00:00:00 2001 From: eguven Date: Sat, 8 Oct 2016 03:06:24 +0100 Subject: [PATCH 086/206] Delete temporary test folders on test exit. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138860196 --- .../exoplayer2/upstream/cache/CachedContentIndexTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index ad64de50ca..4e9171c53b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -40,6 +40,11 @@ public class CachedContentIndexTest extends InstrumentationTestCase { index = new CachedContentIndex(cacheDir); } + @Override + protected void tearDown() throws Exception { + TestUtil.recursiveDelete(cacheDir); + } + public void testAddGetRemove() throws Exception { final String key1 = "key1"; final String key2 = "key2"; From bcc6c9ef43d0cde391204b8c1f20bb2de8686f9d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 11 Nov 2016 06:43:26 -0800 Subject: [PATCH 087/206] Fix a typo. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138871293 --- .../src/main/java/com/google/android/exoplayer2/Timeline.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/Timeline.java index 7d3ad1feae..1b0d03676b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -197,8 +197,8 @@ public abstract class Timeline { public long presentationStartTimeMs; /** - * The windows start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown or - * not applicable. For informational purposes only. + * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown + * or not applicable. For informational purposes only. */ public long windowStartTimeMs; From 6d5d054b45796162f135645539f96b2181bb38a9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 14 Nov 2016 05:56:05 -0800 Subject: [PATCH 088/206] Fix lint in FlacExtractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139064912 --- .../com/google/android/exoplayer2/ext/flac/FlacExtractor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index fd1bb16a4b..9e85620dcd 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -95,7 +95,7 @@ public final class FlacExtractor implements Extractor { if (streamInfo == null) { throw new IOException("Metadata decoding failed"); } - } catch (IOException e){ + } catch (IOException e) { decoderJni.reset(0); input.setRetryPosition(0, e); throw e; // never executes @@ -137,7 +137,7 @@ public final class FlacExtractor implements Extractor { int size; try { size = decoderJni.decodeSample(outputByteBuffer); - } catch (IOException e){ + } catch (IOException e) { if (lastDecodePosition >= 0) { decoderJni.reset(lastDecodePosition); input.setRetryPosition(lastDecodePosition, e); From 74383716a6f6844f25898c0310dfe4dc708775b3 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 14 Nov 2016 11:47:43 -0800 Subject: [PATCH 089/206] Update exoplayerv2 vp9 extension. Reset RenderedFirstFrame when setting OuputBufferRenderer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139100783 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index ddda5fe1f8..2412bf6bf8 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -543,6 +543,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { if (this.outputBufferRenderer == outputBufferRenderer) { return; } + renderedFirstFrame = false; this.outputBufferRenderer = outputBufferRenderer; surface = null; outputMode = (outputBufferRenderer != null) ? VpxDecoder.OUTPUT_MODE_YUV From 348b58021ddd7af22c4d1886994f3bafd1eca930 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 15 Nov 2016 04:49:34 -0800 Subject: [PATCH 090/206] Move underrun detection into AudioTrack. This removes duplication from SimpleDecoderAudioRenderer and MediaCodecAudioRenderer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139187254 --- .../android/exoplayer2/audio/AudioTrack.java | 126 ++++++++++++------ .../audio/MediaCodecAudioRenderer.java | 28 ++-- .../audio/SimpleDecoderAudioRenderer.java | 27 ++-- 3 files changed, 106 insertions(+), 75 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 1eff48e28d..41b0397b01 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioTimestamp; @@ -54,6 +55,24 @@ import java.nio.ByteBuffer; */ public final class AudioTrack { + /** + * Listener for audio track events. + */ + public interface Listener { + + /** + * Called when the audio track underruns. + * + * @param bufferSize The size of the track's buffer, in bytes. + * @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds. + */ + void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -152,6 +171,40 @@ public final class AudioTrack { */ private static final int BUFFER_MULTIPLICATION_FACTOR = 4; + /** + * @see android.media.AudioTrack#PLAYSTATE_STOPPED + */ + private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED; + /** + * @see android.media.AudioTrack#PLAYSTATE_PAUSED + */ + private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED; + /** + * @see android.media.AudioTrack#PLAYSTATE_PLAYING + */ + private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING; + /** + * @see android.media.AudioTrack#ERROR_BAD_VALUE + */ + private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE; + /** + * @see android.media.AudioTrack#MODE_STATIC + */ + private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC; + /** + * @see android.media.AudioTrack#MODE_STREAM + */ + private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM; + /** + * @see android.media.AudioTrack#STATE_INITIALIZED + */ + private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED; + /** + * @see android.media.AudioTrack#WRITE_NON_BLOCKING + */ + @SuppressLint("InlinedApi") + private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING; + private static final String TAG = "AudioTrack"; /** @@ -197,6 +250,7 @@ public final class AudioTrack { private final AudioCapabilities audioCapabilities; private final int streamType; + private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -242,13 +296,18 @@ public final class AudioTrack { private ByteBuffer resampledBuffer; private boolean useResampledBuffer; + private boolean hasData; + private long lastFeedElapsedRealtimeMs; + /** * @param audioCapabilities The current audio capabilities. * @param streamType The type of audio stream for the underlying {@link android.media.AudioTrack}. + * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, int streamType) { + public AudioTrack(AudioCapabilities audioCapabilities, int streamType, Listener listener) { this.audioCapabilities = audioCapabilities; this.streamType = streamType; + this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { @@ -305,7 +364,7 @@ public final class AudioTrack { return CURRENT_POSITION_NOT_SET; } - if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PLAYING) { + if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { maybeSampleSyncParams(); } @@ -424,7 +483,7 @@ public final class AudioTrack { } else { int minBufferSize = android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); - Assertions.checkState(minBufferSize != android.media.AudioTrack.ERROR_BAD_VALUE); + Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; int maxAppBufferSize = (int) Math.max(minBufferSize, @@ -453,11 +512,11 @@ public final class AudioTrack { if (sessionId == SESSION_ID_NOT_SET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM); + targetEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); + targetEncoding, bufferSize, MODE_STREAM, sessionId); } checkAudioTrackInitialized(); @@ -476,42 +535,17 @@ public final class AudioTrack { @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, - channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC, sessionId); + channelConfig, encoding, bufferSize, MODE_STATIC, sessionId); } } } audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); setAudioTrackVolume(); + hasData = false; return sessionId; } - /** - * Returns the size of this {@link AudioTrack}'s buffer in bytes. - *

    - * The value returned from this method may change as a result of calling one of the - * {@link #configure} methods. - * - * @return The size of the buffer in bytes. - */ - public int getBufferSize() { - return bufferSize; - } - - /** - * Returns the size of the buffer in microseconds for PCM {@link AudioTrack}s, or - * {@link C#TIME_UNSET} for passthrough {@link AudioTrack}s. - *

    - * The value returned from this method may change as a result of calling one of the - * {@link #configure} methods. - * - * @return The size of the buffer in microseconds for PCM {@link AudioTrack}s, or - * {@link C#TIME_UNSET} for passthrough {@link AudioTrack}s. - */ - public long getBufferSizeUs() { - return bufferSizeUs; - } - /** * Starts or resumes playing audio if the audio track has been initialized. */ @@ -553,6 +587,18 @@ public final class AudioTrack { * @throws WriteException If an error occurs writing the audio data. */ public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } + int result = writeBuffer(buffer, presentationTimeUs); + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + return result; + } + + private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { boolean isNewSourceBuffer = currentSourceBuffer == null; Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); currentSourceBuffer = buffer; @@ -560,14 +606,14 @@ public final class AudioTrack { if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. - if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { + if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { return 0; } // A new AC-3 audio track's playback position continues to increase from the old track's // position for a short time after is has been released. Avoid writing data until the playback // head position actually returns to zero. - if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED + if (audioTrack.getPlayState() == PLAYSTATE_STOPPED && audioTrackUtil.getPlaybackHeadPosition() != 0) { return 0; } @@ -745,7 +791,7 @@ public final class AudioTrack { latencyUs = 0; resetSyncParams(); int playState = audioTrack.getPlayState(); - if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) { + if (playState == PLAYSTATE_PLAYING) { audioTrack.pause(); } // AudioTrack.release can take some time, so we call it on a background thread. @@ -894,7 +940,7 @@ public final class AudioTrack { */ private void checkAudioTrackInitialized() throws InitializationException { int state = audioTrack.getState(); - if (state == android.media.AudioTrack.STATE_INITIALIZED) { + if (state == STATE_INITIALIZED) { return; } // The track is not successfully initialized. Release and null the track. @@ -952,7 +998,7 @@ public final class AudioTrack { */ private boolean overrideHasPendingData() { return needsPassthroughWorkarounds() - && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED + && audioTrack.getPlayState() == PLAYSTATE_PAUSED && audioTrack.getPlaybackHeadPosition() == 0; } @@ -1063,7 +1109,7 @@ public final class AudioTrack { @TargetApi(21) private static int writeNonBlockingV21( android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { - return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } @TargetApi(21) @@ -1156,7 +1202,7 @@ public final class AudioTrack { } int state = audioTrack.getPlayState(); - if (state == android.media.AudioTrack.PLAYSTATE_STOPPED) { + if (state == PLAYSTATE_STOPPED) { // The audio track hasn't been started. return 0; } @@ -1166,7 +1212,7 @@ public final class AudioTrack { // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 // where the playback head position jumps back to zero on paused passthrough/direct audio // tracks. See [Internal: b/19187573]. - if (state == android.media.AudioTrack.PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { + if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; } rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 5862e7e218..fb793c6a60 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -23,7 +23,6 @@ import android.media.MediaFormat; import android.media.PlaybackParams; import android.media.audiofx.Virtualizer; import android.os.Handler; -import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -43,7 +42,8 @@ import java.nio.ByteBuffer; * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. */ @TargetApi(16) -public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { +public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock, + AudioTrack.Listener { private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; @@ -55,9 +55,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private long currentPositionUs; private boolean allowPositionDiscontinuity; - private boolean audioTrackHasData; - private long lastFeedElapsedRealtimeMs; - /** * @param mediaCodecSelector A decoder selector. */ @@ -136,7 +133,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media int streamType) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - audioTrack = new AudioTrack(audioCapabilities, streamType); + audioTrack = new AudioTrack(audioCapabilities, streamType, this); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -341,29 +338,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } else { audioTrack.initialize(audioSessionId); } - audioTrackHasData = false; } catch (AudioTrack.InitializationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } if (getState() == STATE_STARTED) { audioTrack.play(); } - } else { - // Check for AudioTrack underrun. - boolean audioTrackHadData = audioTrackHasData; - audioTrackHasData = audioTrack.hasPendingData(); - if (audioTrackHadData && !audioTrackHasData && getState() == STATE_STARTED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - long bufferSizeMs = C.usToMs(audioTrack.getBufferSizeUs()); - eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, - elapsedSinceLastFeedMs); - } } int handleBufferResult; try { handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); } catch (AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -408,4 +393,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + // AudioTrack.Listener implementation. + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index f6b1dc14ca..c8bc9066f6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -41,7 +41,8 @@ import com.google.android.exoplayer2.util.Util; /** * Decodes and renders audio using a {@link SimpleDecoder}. */ -public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { +public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock, + AudioTrack.Listener { private final boolean playClearSamplesWithoutKeys; @@ -67,9 +68,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private final AudioTrack audioTrack; private int audioSessionId; - private boolean audioTrackHasData; - private long lastFeedElapsedRealtimeMs; - public SimpleDecoderAudioRenderer() { this(null, null); } @@ -122,7 +120,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - audioTrack = new AudioTrack(audioCapabilities, streamType); + audioTrack = new AudioTrack(audioCapabilities, streamType, this); formatHolder = new FormatHolder(); } @@ -245,24 +243,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } else { audioTrack.initialize(audioSessionId); } - audioTrackHasData = false; if (getState() == STATE_STARTED) { audioTrack.play(); } - } else { - // Check for AudioTrack underrun. - boolean audioTrackHadData = audioTrackHasData; - audioTrackHasData = audioTrack.hasPendingData(); - if (audioTrackHadData && !audioTrackHasData && getState() == STATE_STARTED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - long bufferSizeMs = C.usToMs(audioTrack.getBufferSizeUs()); - eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, - elapsedSinceLastFeedMs); - } } int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); // If we are out of sync, allow currentPositionUs to jump backwards. if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { @@ -493,4 +479,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + // AudioTrack.Listener implementation. + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } From 21e3361dfe8fea5b82be3656af29d032dbab7c33 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 15 Nov 2016 08:30:17 -0800 Subject: [PATCH 091/206] Move 3 private methods to the end of the class ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139204000 --- .../exoplayer2/metadata/id3/Id3Decoder.java | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 05bff672a4..b71828c7fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -93,40 +93,6 @@ public final class Id3Decoder implements MetadataDecoder { return new Metadata(id3Frames); } - // TODO: Move the following three methods nearer to the bottom of the file. - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { - int terminationPos = indexOfZeroByte(data, fromIndex); - - // For single byte encoding charsets, we're done. - if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { - return terminationPos; - } - - // Otherwise ensure an even index and look for a second zero byte. - while (terminationPos < data.length - 1) { - if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { - return terminationPos; - } - terminationPos = indexOfZeroByte(data, terminationPos + 1); - } - - return data.length; - } - - private static int indexOfZeroByte(byte[] data, int fromIndex) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] == (byte) 0) { - return i; - } - } - return data.length; - } - - private static int delimiterLength(int encodingByte) { - return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) - ? 1 : 2; - } - /** * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. @@ -510,6 +476,39 @@ public final class Id3Decoder implements MetadataDecoder { } } + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise ensure an even index and look for a second zero byte. + while (terminationPos < data.length - 1) { + if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + private static final class Id3Header { private final int majorVersion; From 0effffb89fa02ac711eadcf3e167c9ea65783ab1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 15 Nov 2016 09:31:24 -0800 Subject: [PATCH 092/206] Add support for reading .mp3 boxes in stsd. This is used by Quicktime for MP3 tracks. Issue: #2066 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139209989 --- .../com/google/android/exoplayer2/extractor/mp4/Atom.java | 1 + .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 749c9b3542..8291fd5efc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -52,6 +52,7 @@ import java.util.List; public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); + public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3"); public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 47cb3262e1..b8d5d634ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -600,7 +600,8 @@ import java.util.List; || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb - || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt) { + || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE__mp3) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_TTML) { @@ -829,6 +830,8 @@ import java.util.List; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + } else if (atomType == Atom.TYPE__mp3) { + mimeType = MimeTypes.AUDIO_MPEG; } byte[] initializationData = null; From d6eb9cb79f8dead17a789b688664794aba2290ef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 16 Nov 2016 08:04:53 -0800 Subject: [PATCH 093/206] Allow playlists of different size in HlsPlaylistTracker playlist adjustment Issue:#2059 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139331670 --- .../hls/playlist/HlsPlaylistTracker.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 53841e1b69..13e64f5e42 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -294,21 +294,23 @@ public final class HlsPlaylistTracker implements Loader.Callback oldSegments = oldPlaylist.segments; int oldPlaylistSize = oldSegments.size(); - if (newSegmentsCount <= oldPlaylistSize) { - ArrayList newSegments = new ArrayList<>(); + int newPlaylistSize = newPlaylist.segments.size(); + int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; + if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0 + && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { + // Playlist has not changed. + return oldPlaylist; + } + if (mediaSequenceOffset <= oldPlaylistSize) { // We can extrapolate the start time of new segments from the segments of the old snapshot. - int newPlaylistSize = newPlaylist.segments.size(); - for (int i = newSegmentsCount; i < oldPlaylistSize; i++) { + ArrayList newSegments = new ArrayList<>(newPlaylistSize); + for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) { newSegments.add(oldSegments.get(i)); } HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); - for (int i = newPlaylistSize - newSegmentsCount; i < newPlaylistSize; i++) { + for (int i = newSegments.size(); i < newPlaylistSize; i++) { lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( lastSegment.startTimeUs + lastSegment.durationUs); newSegments.add(lastSegment); From 92d34cd877ea30a912ebc10531ffdd8d747815cc Mon Sep 17 00:00:00 2001 From: cblay Date: Wed, 16 Nov 2016 14:14:12 -0800 Subject: [PATCH 094/206] Add flag to CacheDataSource to disable caching unset length requests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139377417 --- .../exoplayer2/upstream/cache/CacheDataSource.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index b98eadc4cc..4dc5431b47 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -49,7 +49,8 @@ public final class CacheDataSource implements DataSource { * Flags controlling the cache's behavior. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR}) + @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR, + FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}) public @interface Flags {} /** * A flag indicating whether we will block reads if the cache key is locked. If this flag is @@ -64,6 +65,11 @@ public final class CacheDataSource implements DataSource { */ public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; + /** + * A flag indicating that the cache should be bypassed for requests whose lengths are unset. + */ + public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; + /** * Listener of {@link CacheDataSource} events. */ @@ -87,6 +93,7 @@ public final class CacheDataSource implements DataSource { private final boolean blockOnCache; private final boolean ignoreCacheOnError; + private final boolean ignoreCacheForUnsetLengthRequests; private DataSource currentDataSource; private boolean currentRequestUnbounded; @@ -146,6 +153,8 @@ public final class CacheDataSource implements DataSource { this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0; + this.ignoreCacheForUnsetLengthRequests = + (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0; this.upstreamDataSource = upstream; if (cacheWriteDataSink != null) { this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink); @@ -162,7 +171,8 @@ public final class CacheDataSource implements DataSource { flags = dataSpec.flags; key = dataSpec.key != null ? dataSpec.key : uri.toString(); readPosition = dataSpec.position; - currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError; + currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) + || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { bytesRemaining = dataSpec.length; } else { From 2add12d5f78e7ffe96f376fa4ae0516413175f97 Mon Sep 17 00:00:00 2001 From: yutingtseng Date: Wed, 16 Nov 2016 14:49:20 -0800 Subject: [PATCH 095/206] Update CacheDataSink to optionally use a BufferedOutputStream for writing ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139381958 --- .../upstream/cache/CacheDataSink.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 6a301f8a2e..22ec21374d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -21,9 +21,11 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; /** * Writes data into a cache. @@ -32,10 +34,12 @@ public final class CacheDataSink implements DataSink { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; private DataSpec dataSpec; private File file; - private FileOutputStream outputStream; + private OutputStream outputStream; + private FileOutputStream underlyingFileOutputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; @@ -57,8 +61,21 @@ public final class CacheDataSink implements DataSink { * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, 0); + } + + /** + * @param cache The cache into which data should be written. + * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for + * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into + * multiple cache files. + * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative + * value disables buffering. + */ + public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = Assertions.checkNotNull(cache); this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override @@ -114,7 +131,10 @@ public final class CacheDataSink implements DataSink { private void openNextOutputStream() throws IOException { file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); - outputStream = new FileOutputStream(file); + underlyingFileOutputStream = new FileOutputStream(file); + outputStream = bufferSize > 0 + ? new BufferedOutputStream(underlyingFileOutputStream, bufferSize) + : underlyingFileOutputStream; outputStreamBytesWritten = 0; } @@ -126,7 +146,7 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - outputStream.getFD().sync(); + underlyingFileOutputStream.getFD().sync(); success = true; } finally { Util.closeQuietly(outputStream); From a8a2ef4a24a5e5bdd86309195a0ede904ef5e0e2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 17 Nov 2016 03:58:01 -0800 Subject: [PATCH 096/206] Blacklist HLS media playlists that return 4xx error codes Issue:#87 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139443476 --- .../exoplayer2/source/hls/HlsChunkSource.java | 39 ++++++++++++++----- .../exoplayer2/source/hls/HlsMediaPeriod.java | 8 ++++ .../source/hls/HlsSampleStreamWrapper.java | 5 +++ .../hls/playlist/HlsPlaylistTracker.java | 25 +++++++++--- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index cbef07f6fe..9e39b9adb5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -33,7 +33,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.chunk.DataChunk; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; @@ -76,7 +76,7 @@ import java.util.Locale; /** * Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public HlsMasterPlaylist.HlsUrl playlist; + public HlsUrl playlist; /** * Clears the holder. @@ -99,7 +99,7 @@ import java.util.Locale; private final DataSource dataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final HlsMasterPlaylist.HlsUrl[] variants; + private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; @@ -125,7 +125,7 @@ import java.util.Locale; * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. */ - public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsMasterPlaylist.HlsUrl[] variants, + public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { this.playlistTracker = playlistTracker; this.variants = variants; @@ -183,7 +183,7 @@ import java.util.Locale; * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to - * contain the {@link HlsMasterPlaylist.HlsUrl} that refers to the playlist that needs refreshing. + * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position. If {@code previous} is null then this @@ -198,6 +198,8 @@ import java.util.Locale; // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 : Math.max(0, previous.getAdjustedStartTimeUs() - playbackPositionUs); + + // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); @@ -209,6 +211,7 @@ import java.util.Locale; return; } + // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; @@ -244,6 +247,7 @@ import java.util.Locale; return; } + // Handle encryption. HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); // Check if encryption is specified. @@ -272,7 +276,7 @@ import java.util.Locale; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - // Configure the extractor that will read the chunk. + // Set the extractor that will read the chunk. Extractor extractor; boolean useInitializedExtractor = lastLoadedInitializationChunk != null && lastLoadedInitializationChunk.format == format; @@ -343,6 +347,7 @@ import java.util.Locale; extractorNeedsInit = false; } + // Initialize the extractor. if (needNewExtractor && mediaPlaylist.initializationSegment != null && !useInitializedExtractor) { out.chunk = buildInitializationChunk(mediaPlaylist, extractor, format); @@ -388,12 +393,28 @@ import java.util.Locale; * * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. - * @param e The error. + * @param error The error. * @return Whether the load should be canceled. */ - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException e) { + public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) { return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), e); + trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error); + } + + /** + * Called when an error is encountered while loading a playlist. + * + * @param url The url that references the playlist whose load encountered the error. + * @param error The error. + */ + public void onPlaylistLoadError(HlsUrl url, IOException error) { + int trackGroupIndex = trackGroup.indexOf(url.format); + if (trackGroupIndex == C.INDEX_UNSET) { + // The url is not handled by this chunk source. + return; + } + ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, + trackSelection.indexOf(trackGroupIndex), error); } // Private methods. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 0c27b3df7d..3951b30a78 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -268,6 +268,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + @Override + public void onPlaylistLoadError(HlsMasterPlaylist.HlsUrl url, IOException error) { + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + sampleStreamWrapper.onPlaylistLoadError(url, error); + } + callback.onContinueLoadingRequested(this); + } + // Internal methods. private void buildAndPrepareSampleStreamWrappers() { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index c491dc9760..1bcc7fe878 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; @@ -274,6 +275,10 @@ import java.util.LinkedList; return largestQueuedTimestampUs; } + public void onPlaylistLoadError(HlsUrl url, IOException error) { + chunkSource.onPlaylistLoadError(url, error); + } + // SampleStream implementation. /* package */ boolean isReady(int group) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 13e64f5e42..abad300f70 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -61,6 +61,14 @@ public final class HlsPlaylistTracker implements Loader.Callback loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - // TODO: Add support for playlist blacklisting in response to server error codes. + // TODO: Change primary playlist if this is the primary playlist bundle. boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + if (callback != null) { + callback.onPlaylistLoadError(playlistUrl, error); + } + if (isFatal) { + return Loader.DONT_RETRY_FATAL; + } else { + return primaryHlsUrl == playlistUrl ? Loader.RETRY : Loader.DONT_RETRY; + } } // Runnable implementation. From 051be5c588b25b2d9d899ae89b195455007e3bb3 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 17 Nov 2016 04:35:54 -0800 Subject: [PATCH 097/206] Use buffers to speed up cache index file io. Use BufferedOutputStream and BufferedInputStream while writing / reading. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139446039 --- .../exoplayer2/upstream/cache/CachedContentIndex.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 47019b98a3..64863ac42b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.Util; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -207,7 +209,7 @@ import javax.crypto.spec.SecretKeySpec; private boolean readFile() { DataInputStream input = null; try { - InputStream inputStream = atomicFile.openRead(); + InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); input = new DataInputStream(inputStream); int version = input.readInt(); if (version != VERSION) { @@ -254,7 +256,7 @@ import javax.crypto.spec.SecretKeySpec; private void writeFile() throws CacheException { DataOutputStream output = null; try { - OutputStream outputStream = atomicFile.startWrite(); + OutputStream outputStream = new BufferedOutputStream(atomicFile.startWrite()); output = new DataOutputStream(outputStream); output.writeInt(VERSION); From 61c9c169542204fde21a11cc479cd7a80c4fc71c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Nov 2016 05:59:32 -0800 Subject: [PATCH 098/206] Make sure we report video size after Surface is set Issue: #2077 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139451511 --- .../ext/vp9/LibvpxVideoRenderer.java | 58 +++++++++---------- .../video/MediaCodecVideoRenderer.java | 34 ++++++----- .../video/VideoRendererEventListener.java | 3 +- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 2412bf6bf8..e4cc2ae3ce 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; @@ -85,8 +86,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private boolean inputStreamEnded; private boolean outputStreamEnded; - private int previousWidth; - private int previousHeight; + private int lastReportedWidth; + private int lastReportedHeight; private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; @@ -146,8 +147,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; joiningDeadlineMs = -1; - previousWidth = -1; - previousHeight = -1; + clearLastReportedVideoSize(); formatHolder = new FormatHolder(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; @@ -446,6 +446,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { outputBuffer = null; format = null; waitingForKeys = false; + clearLastReportedVideoSize(); try { releaseDecoder(); } finally { @@ -520,35 +521,29 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { - setSurface((Surface) message); + setOutput((Surface) message, null); } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) { - setOutputBufferRenderer((VpxOutputBufferRenderer) message); + setOutput(null, (VpxOutputBufferRenderer) message); } else { super.handleMessage(messageType, message); } } - private void setSurface(Surface surface) { - if (this.surface == surface) { - return; - } + private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) { + // At most one output may be non-null. Both may be null if the output is being cleared. + Assertions.checkState(surface == null || outputBufferRenderer == null); + // Clear state so that we always call the event listener with the video size and when a frame + // is rendered, even if the output hasn't changed. renderedFirstFrame = false; - this.surface = surface; - outputBufferRenderer = null; - outputMode = (surface != null) ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE; - updateDecoder(); - } - - private void setOutputBufferRenderer(VpxOutputBufferRenderer outputBufferRenderer) { - if (this.outputBufferRenderer == outputBufferRenderer) { - return; + clearLastReportedVideoSize(); + // We only need to update the decoder if the output has changed. + if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) { + this.surface = surface; + this.outputBufferRenderer = outputBufferRenderer; + outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV + : surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE; + updateDecoder(); } - renderedFirstFrame = false; - this.outputBufferRenderer = outputBufferRenderer; - surface = null; - outputMode = (outputBufferRenderer != null) ? VpxDecoder.OUTPUT_MODE_YUV - : VpxDecoder.OUTPUT_MODE_NONE; - updateDecoder(); } private void updateDecoder() { @@ -565,10 +560,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return surface != null || outputBufferRenderer != null; } - private void maybeNotifyVideoSizeChanged(final int width, final int height) { - if (previousWidth != width || previousHeight != height) { - previousWidth = width; - previousHeight = height; + private void clearLastReportedVideoSize() { + lastReportedWidth = Format.NO_VALUE; + lastReportedHeight = Format.NO_VALUE; + } + + private void maybeNotifyVideoSizeChanged(int width, int height) { + if (lastReportedWidth != width || lastReportedHeight != height) { + lastReportedWidth = width; + lastReportedHeight = height; eventDispatcher.videoSizeChanged(width, height, 0, 1); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index c3921b4d6a..b94beb10f6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -163,9 +163,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; - lastReportedPixelWidthHeightRatio = Format.NO_VALUE; + clearLastReportedVideoSize(); } @Override @@ -272,9 +270,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; - lastReportedPixelWidthHeightRatio = Format.NO_VALUE; + clearLastReportedVideoSize(); frameReleaseTimeHelper.disable(); try { super.onDisabled(); @@ -294,15 +290,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { - if (this.surface == surface) { - return; - } + // Clear state so that we always call the event listener with the video size and when a frame + // is rendered, even if the surface hasn't changed. renderedFirstFrame = false; - this.surface = surface; - int state = getState(); - if (state == STATE_ENABLED || state == STATE_STARTED) { - releaseCodec(); - maybeInitCodec(); + clearLastReportedVideoSize(); + // We only need to actually release and reinitialize the codec if the surface has changed. + if (this.surface != surface) { + this.surface = surface; + int state = getState(); + if (state == STATE_ENABLED || state == STATE_STARTED) { + releaseCodec(); + maybeInitCodec(); + } } } @@ -584,6 +583,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return (maxPixels * 3) / (2 * minCompressionRatio); } + private void clearLastReportedVideoSize() { + lastReportedWidth = Format.NO_VALUE; + lastReportedHeight = Format.NO_VALUE; + lastReportedPixelWidthHeightRatio = Format.NO_VALUE; + lastReportedUnappliedRotationDegrees = Format.NO_VALUE; + } + private void maybeNotifyVideoSizeChanged() { if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees diff --git a/library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 4c7d8a62c1..53d6a76b8d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -69,7 +69,8 @@ public interface VideoRendererEventListener { void onDroppedFrames(int count, long elapsedMs); /** - * Called each time there's a change in the size of the video being rendered. + * Called before a frame is rendered for the first time since setting the surface, and each time + * there's a change in the size, rotation or pixel aspect ratio of the video being rendered. * * @param width The video width in pixels. * @param height The video height in pixels. From f57434006f49bc5dd5dfb620e292287f4cce89da Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Nov 2016 06:02:39 -0800 Subject: [PATCH 099/206] Fix potential NPE in ExoPlayerImplInternal I'll have a more thorough refactor of some of this class fairly soon! ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139451693 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 743015509b..c47dc71209 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -918,7 +918,7 @@ import java.io.IOException; Object uid = loadingPeriodHolder.uid; int index = timeline.getIndexOfPeriod(uid); if (index == C.INDEX_UNSET) { - boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); + boolean restarted = attemptRestart(loadingPeriodHolder.index, oldTimeline, timeline); finishSourceInfoRefresh(manifest, true); if (!restarted) { // TODO: We should probably propagate an error here. From 1a62dad98065b9b3418c82c552a5ac8e66891c53 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 17 Nov 2016 06:32:08 -0800 Subject: [PATCH 100/206] Remove TODO in ffmpeg extension README.md. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139453745 --- extensions/ffmpeg/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index f0ce07bdf7..d7c5e21fcc 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -31,7 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" NDK_PATH="" ``` -* Fetch and build ffmpeg. +* Fetch and build FFmpeg. For example, to fetch and build for armv7a: @@ -75,7 +75,7 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 ``` -TODO: Add instructions for other ABIs. +Repeat these steps for any other architectures you need to support. * In your project, you can add a dependency on the extension by using a rule like this: From 35054f8f7ca65d9060e1b4924140c7addf12743d Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 17 Nov 2016 14:15:45 -0800 Subject: [PATCH 101/206] Added ReusableBufferedOutputStream. ReusableBufferedOutputStream is a subclass of BufferedOutputStream with a reset(OutputStream) that allows an instance to be re-used with another underlying output stream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139505999 --- .../ReusableBufferedOutputStreamTest.java | 46 ++++++++++++ .../util/ReusableBufferedOutputStream.java | 70 +++++++++++++++++++ .../google/android/exoplayer2/util/Util.java | 13 ++++ 3 files changed, 129 insertions(+) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java new file mode 100644 index 0000000000..beb9e44853 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 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.util; + +import android.test.MoreAsserts; +import java.io.ByteArrayOutputStream; +import junit.framework.TestCase; + +/** + * Tests {@link ReusableBufferedOutputStream}. + */ +public class ReusableBufferedOutputStreamTest extends TestCase { + + private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); + private static final byte[] TEST_DATA_2 = "2 test data".getBytes(); + + public void testReset() throws Exception { + ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); + ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( + byteArrayOutputStream1, 1000); + outputStream.write(TEST_DATA_1); + outputStream.close(); + + ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000); + outputStream.reset(byteArrayOutputStream2); + outputStream.write(TEST_DATA_2); + outputStream.close(); + + MoreAsserts.assertEquals(TEST_DATA_1, byteArrayOutputStream1.toByteArray()); + MoreAsserts.assertEquals(TEST_DATA_2, byteArrayOutputStream2.toByteArray()); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java new file mode 100644 index 0000000000..1ae947b610 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 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.util; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This is a subclass of {@link BufferedOutputStream} with a {@link #reset(OutputStream)} method + * that allows an instance to be re-used with another underlying output stream. + */ +public final class ReusableBufferedOutputStream extends BufferedOutputStream { + + public ReusableBufferedOutputStream(OutputStream out) { + super(out); + } + + public ReusableBufferedOutputStream(OutputStream out, int size) { + super(out, size); + } + + @Override + public void close() throws IOException { + Throwable thrown = null; + try { + flush(); + } catch (Throwable e) { + thrown = e; + } + + try { + out.close(); + } catch (Throwable e) { + if (thrown == null) { + thrown = e; + } + } + out = null; + + if (thrown != null) { + Util.sneakyThrow(thrown); + } + } + + /** + * Resets this stream and uses the given output stream for writing. This stream must be closed + * before resetting. + * + * @param out New output stream to be used for writing. + * @throws IllegalStateException If the stream isn't closed. + */ + public void reset(OutputStream out) { + Assertions.checkState(this.out == null); + this.out = out; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 1c25c5da09..6762442b7c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -840,6 +840,19 @@ public final class Util { return builder.toString(); } + /** + * A hacky method that always throws {@code t} even if {@code t} is a checked exception, + * and is not declared to be thrown. + */ + public static void sneakyThrow(Throwable t) { + Util.sneakyThrowInternal(t); + } + + @SuppressWarnings("unchecked") + private static void sneakyThrowInternal(Throwable t) throws T { + throw (T) t; + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. From d890c2f48f1ed541781872b3df12abe36a41db6a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 18 Nov 2016 02:07:17 -0800 Subject: [PATCH 102/206] Fix infinite loop -> ANR at end of HLS playbacks continueLoading shouldn't return true unless it's done something. Always returning true if endOfStream was causing CompositeSequenceableLoader.continueLoading to loop forever. It looks like the same issue exists in ChunkSampleStream as well, although I can't seem to provoke DASH or SS playbacks into doing anything bad as a result. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139559834 --- .../android/exoplayer2/source/chunk/ChunkSampleStream.java | 2 +- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index bb2f9b214b..6de7c6ec01 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -251,7 +251,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean continueLoading(long positionUs) { - if (loader.isLoading()) { + if (loadingFinished || loader.isLoading()) { return false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 1bcc7fe878..863b06ec38 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -329,7 +329,7 @@ import java.util.LinkedList; @Override public boolean continueLoading(long positionUs) { - if (loader.isLoading()) { + if (loadingFinished || loader.isLoading()) { return false; } From cafe603694b47b4e4096648681dfe01721eedbaf Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 18 Nov 2016 03:30:44 -0800 Subject: [PATCH 103/206] Allow regressing media sequence numbers in HLS media playlists This is techically not allowed by the spec[1] but might still occur in certain scenarios. New playlists with older media sequence numbers are ignored. [1]: HLS draft version 20, section-6.2.1 Issue:#2059 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139564889 --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index abad300f70..e368b1c37f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -311,6 +311,10 @@ public final class HlsPlaylistTracker implements Loader.Callback newSegments = new ArrayList<>(newPlaylistSize); From 4a30dff5241c759934ca287fb0838c35df0ed5b3 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 18 Nov 2016 04:07:20 -0800 Subject: [PATCH 104/206] Fix NPE in TextTrackRenderer Issue: #2081 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139566990 --- .../android/exoplayer2/text/TextRenderer.java | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index e28363f9e4..8dbde1be5e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -114,19 +114,11 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { + clearOutput(); + resetBuffers(); + decoder.flush(); inputStreamEnded = false; outputStreamEnded = false; - if (subtitle != null) { - subtitle.release(); - subtitle = null; - } - if (nextSubtitle != null) { - nextSubtitle.release(); - nextSubtitle = null; - } - nextInputBuffer = null; - clearOutput(); - decoder.flush(); } @Override @@ -220,18 +212,10 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onDisabled() { - if (subtitle != null) { - subtitle.release(); - subtitle = null; - } - if (nextSubtitle != null) { - nextSubtitle.release(); - nextSubtitle = null; - } + clearOutput(); + resetBuffers(); decoder.release(); decoder = null; - nextInputBuffer = null; - clearOutput(); super.onDisabled(); } @@ -247,6 +231,19 @@ public final class TextRenderer extends BaseRenderer implements Callback { return true; } + private void resetBuffers() { + nextInputBuffer = null; + nextSubtitleEventIndex = C.INDEX_UNSET; + if (subtitle != null) { + subtitle.release(); + subtitle = null; + } + if (nextSubtitle != null) { + nextSubtitle.release(); + nextSubtitle = null; + } + } + private long getNextEventTime() { return ((nextSubtitleEventIndex == C.INDEX_UNSET) || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE From 6dbfdecbe00c8a09f97935889b530697074ea7cb Mon Sep 17 00:00:00 2001 From: ccwu Date: Fri, 18 Nov 2016 14:56:39 -0800 Subject: [PATCH 105/206] Let the mp4 extractor support "camm" metadata tracks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139626848 --- .../com/google/android/exoplayer2/extractor/mp4/Atom.java | 1 + .../android/exoplayer2/extractor/mp4/AtomParsers.java | 6 ++++++ .../java/com/google/android/exoplayer2/util/MimeTypes.java | 1 + 3 files changed, 8 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 8291fd5efc..2eac7926e7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -133,6 +133,7 @@ import java.util.List; public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); + public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); public final int type; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index b8d5d634ed..4272cdaa11 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -50,6 +50,7 @@ import java.util.List; private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc"); + private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); /** * Parses a trak atom (defined in 14496-12). @@ -541,6 +542,8 @@ import java.util.List; } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt || trackType == TYPE_clcp) { return C.TRACK_TYPE_TEXT; + } else if (trackType == TYPE_meta) { + return C.TRACK_TYPE_METADATA; } else { return C.TRACK_TYPE_UNKNOWN; } @@ -621,6 +624,9 @@ import java.util.List; out.format = Format.createTextSampleFormat(Integer.toString(trackId), MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, language, drmInitData); out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else if (childAtomType == Atom.TYPE_camm) { + out.format = Format.createSampleFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); } stsd.setPosition(childStartPosition + childAtomSize); } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 641dd670af..b690362fb7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -75,6 +75,7 @@ public final class MimeTypes { public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; + public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; private MimeTypes() {} From adc9dd1c75e18f0dc59c32169609090bcd3737a8 Mon Sep 17 00:00:00 2001 From: dsantoro Date: Sun, 20 Nov 2016 18:12:32 -0800 Subject: [PATCH 106/206] Add null checks to closeQuietly calls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139738429 --- .../java/com/google/android/exoplayer2/util/Util.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 6762442b7c..cbaebc2b55 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -210,7 +210,9 @@ public final class Util { */ public static void closeQuietly(DataSource dataSource) { try { - dataSource.close(); + if (dataSource != null) { + dataSource.close(); + } } catch (IOException e) { // Ignore. } @@ -224,7 +226,9 @@ public final class Util { */ public static void closeQuietly(Closeable closeable) { try { - closeable.close(); + if (closeable != null) { + closeable.close(); + } } catch (IOException e) { // Ignore. } From 060ca4aecc11c850ccfbbfea7cef7f1bff9a8859 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Mar 2016 05:35:48 +0000 Subject: [PATCH 107/206] ExoPlayerImplInternal cleanup - Make handleSourceInfoRefreshed clearer. - Remove bufferAheadPeriodCount. Seems error prone. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139774580 --- .../exoplayer2/ExoPlayerImplInternal.java | 249 ++++++++---------- 1 file changed, 111 insertions(+), 138 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index c47dc71209..f8cacb43a7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -63,6 +63,13 @@ import java.io.IOException; bufferedPositionUs = startPositionUs; } + public PlaybackInfo copyWithPeriodIndex(int periodIndex) { + PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs); + playbackInfo.positionUs = positionUs; + playbackInfo.bufferedPositionUs = bufferedPositionUs; + return playbackInfo; + } + } public static final class TrackInfo { @@ -161,7 +168,6 @@ import java.io.IOException; private boolean isTimelineReady; private boolean isTimelineEnded; - private int bufferAheadPeriodCount; private MediaPeriodHolder playingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder loadingPeriodHolder; @@ -603,7 +609,6 @@ import java.io.IOException; } // Update loaded periods. - bufferAheadPeriodCount = 0; if (newPlayingPeriodHolder != null) { newPlayingPeriodHolder.next = null; setPlayingPeriodHolder(newPlayingPeriodHolder); @@ -679,7 +684,6 @@ import java.io.IOException; readingPeriodHolder = null; loadingPeriodHolder = null; timeline = null; - bufferAheadPeriodCount = 0; setIsLoading(false); } @@ -737,11 +741,10 @@ import java.io.IOException; playingPeriodHolder.next = null; readingPeriodHolder = playingPeriodHolder; loadingPeriodHolder = playingPeriodHolder; - bufferAheadPeriodCount = 0; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( - playbackInfo.positionUs, loadControl, recreateStreams, streamResetFlags); + playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { playbackInfo.positionUs = periodPositionUs; resetRendererPosition(periodPositionUs); @@ -786,13 +789,12 @@ import java.io.IOException; while (periodHolder != null) { periodHolder.release(); periodHolder = periodHolder.next; - bufferAheadPeriodCount--; } loadingPeriodHolder.next = null; if (loadingPeriodHolder.prepared) { long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, loadingPeriodHolder.toPeriodTime(rendererPositionUs)); - loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, loadControl, false); + loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } maybeContinueLoading(); @@ -832,7 +834,7 @@ import java.io.IOException; } private void handleSourceInfoRefreshed(Pair timelineAndManifest) - throws ExoPlaybackException, IOException { + throws ExoPlaybackException { Timeline oldTimeline = timeline; timeline = timelineAndManifest.first; Object manifest = timelineAndManifest.second; @@ -841,9 +843,9 @@ import java.io.IOException; if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); if (periodPosition == null) { - // TODO: We should probably propagate an error here. // We failed to resolve the seek position. Stop the player. finishSourceInfoRefresh(manifest, false); + // TODO: We should probably propagate an error here. stopInternal(); return; } @@ -854,107 +856,88 @@ import java.io.IOException; } } - // Update the loaded periods to take into account the new timeline. - if (playingPeriodHolder != null) { - int index = timeline.getIndexOfPeriod(playingPeriodHolder.uid); - if (index == C.INDEX_UNSET) { - boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline); - finishSourceInfoRefresh(manifest, true); - if (!restarted) { - // TODO: We should probably propagate an error here. - stopInternal(); - } - return; + MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder + : loadingPeriodHolder; + if (periodHolder == null) { + // We don't have any period holders, so we're done. + finishSourceInfoRefresh(manifest, true); + return; + } + + int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); + if (periodIndex == C.INDEX_UNSET) { + // We didn't find the current period in the new timeline. Attempt to restart. + boolean restarted = attemptRestart(periodHolder, oldTimeline, timeline); + finishSourceInfoRefresh(manifest, true); + if (!restarted) { + // TODO: We should probably propagate an error here. + stopInternal(); } + return; + } - // The playing period is also in the new timeline. Update the index for each loaded period - // until a period is found that does not match the old timeline. - timeline.getPeriod(index, period, true); - playingPeriodHolder.setIndex(timeline, timeline.getWindow(period.windowIndex, window), - index); + // The current period is in the new timeline. Update the holder and playbackInfo. + timeline.getPeriod(periodIndex, period); + boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 + && !timeline.getWindow(period.windowIndex, window).isDynamic; + periodHolder.setIndex(periodIndex, isLastPeriod); + if (periodIndex != playbackInfo.periodIndex) { + playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + } - MediaPeriodHolder previousPeriodHolder = playingPeriodHolder; - boolean seenReadingPeriod = false; - bufferAheadPeriodCount = 0; - while (previousPeriodHolder.next != null) { - MediaPeriodHolder periodHolder = previousPeriodHolder.next; - index++; - timeline.getPeriod(index, period, true); - if (!periodHolder.uid.equals(period.uid)) { - if (!seenReadingPeriod) { - // Renderers may have read a period that has been removed, so release all loaded periods - // and seek to the current position of the playing period index. - index = playingPeriodHolder.index; - releasePeriodHoldersFrom(playingPeriodHolder); - playingPeriodHolder = null; - readingPeriodHolder = null; - loadingPeriodHolder = null; - long newPositionUs = seekToPeriodPosition(index, playbackInfo.positionUs); - if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = new PlaybackInfo(index, newPositionUs); - } - finishSourceInfoRefresh(manifest, true); - return; + // If there are subsequent holders, update the index for each of them. If we find a holder + // that's inconsistent with the new timeline then take appropriate action. + boolean seenReadingPeriod = false; + while (periodHolder.next != null) { + MediaPeriodHolder previousPeriodHolder = periodHolder; + periodHolder = periodHolder.next; + periodIndex++; + timeline.getPeriod(periodIndex, period, true); + isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 + && !timeline.getWindow(period.windowIndex, window).isDynamic; + if (periodHolder.uid.equals(period.uid)) { + // The holder is consistent with the new timeline. Update its index and continue. + periodHolder.setIndex(periodIndex, isLastPeriod); + seenReadingPeriod |= (periodHolder == readingPeriodHolder); + } else { + // The holder is inconsistent with the new timeline. + if (!seenReadingPeriod) { + // Renderers may have read from a period that's been removed, so release all loaded + // periods and seek to the current position of the playing period index. + periodIndex = playingPeriodHolder.index; + releasePeriodHoldersFrom(playingPeriodHolder); + playingPeriodHolder = null; + readingPeriodHolder = null; + loadingPeriodHolder = null; + long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); } - - // Update the loading period to be the latest period that is still valid. + } else { + // Update the loading period to be the last period that's still valid, and release all + // subsequent periods. loadingPeriodHolder = previousPeriodHolder; loadingPeriodHolder.next = null; - // Release the rest of the timeline. releasePeriodHoldersFrom(periodHolder); - break; } - - bufferAheadPeriodCount++; - int windowIndex = timeline.getPeriod(index, period).windowIndex; - periodHolder.setIndex(timeline, timeline.getWindow(windowIndex, window), index); - if (periodHolder == readingPeriodHolder) { - seenReadingPeriod = true; - } - previousPeriodHolder = periodHolder; - } - } else if (loadingPeriodHolder != null) { - Object uid = loadingPeriodHolder.uid; - int index = timeline.getIndexOfPeriod(uid); - if (index == C.INDEX_UNSET) { - boolean restarted = attemptRestart(loadingPeriodHolder.index, oldTimeline, timeline); - finishSourceInfoRefresh(manifest, true); - if (!restarted) { - // TODO: We should probably propagate an error here. - stopInternal(); - } - return; - } else { - int windowIndex = timeline.getPeriod(index, this.period).windowIndex; - loadingPeriodHolder.setIndex(timeline, timeline.getWindow(windowIndex, window), - index); + break; } } - if (oldTimeline != null) { - int newPlayingIndex = playingPeriodHolder != null ? playingPeriodHolder.index - : loadingPeriodHolder != null ? loadingPeriodHolder.index : C.INDEX_UNSET; - if (newPlayingIndex != C.INDEX_UNSET - && newPlayingIndex != playbackInfo.periodIndex) { - playbackInfo = new PlaybackInfo(newPlayingIndex, playbackInfo.positionUs); - updatePlaybackPositions(); - } - } finishSourceInfoRefresh(manifest, true); } - private boolean attemptRestart(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { - int newPeriodIndex = resolveSubsequentPeriod(oldPeriodIndex, oldTimeline, newTimeline); + private boolean attemptRestart(MediaPeriodHolder oldPeriodHolder, Timeline oldTimeline, + Timeline newTimeline) { + int newPeriodIndex = resolveSubsequentPeriod(oldPeriodHolder.index, oldTimeline, newTimeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to find a replacement period. Stop the player. return false; } // Release all loaded periods. - releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder); - bufferAheadPeriodCount = 0; + releasePeriodHoldersFrom(oldPeriodHolder); playingPeriodHolder = null; readingPeriodHolder = null; loadingPeriodHolder = null; @@ -1095,7 +1078,8 @@ import java.io.IOException; if (loadingPeriodHolder == null || (loadingPeriodHolder.isFullyBuffered() && !loadingPeriodHolder.isLast - && bufferAheadPeriodCount < MAXIMUM_BUFFER_AHEAD_PERIODS)) { + && (playingPeriodHolder == null + || loadingPeriodHolder.index - playingPeriodHolder.index < MAXIMUM_BUFFER_AHEAD_PERIODS))) { // We don't have a loading period or it's fully loaded, so try and create the next one. int newLoadingPeriodIndex = loadingPeriodHolder == null ? playbackInfo.periodIndex : loadingPeriodHolder.index + 1; @@ -1119,9 +1103,9 @@ import java.io.IOException; // transitions is equal the duration of media that's currently buffered (assuming no // interruptions). Hence we project the default start position forward by the duration of // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = loadingPeriodHolder.rendererPositionOffsetUs + long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - loadingPeriodHolder.startPositionUs - rendererPositionUs; + - rendererPositionUs; Pair defaultPosition = getPeriodPosition(timeline, windowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); if (defaultPosition == null) { @@ -1133,24 +1117,20 @@ import java.io.IOException; } } if (newLoadingPeriodIndex != C.INDEX_UNSET) { - Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid; - MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, - loadControl.getAllocator(), periodStartPositionUs); - newMediaPeriod.prepare(this); + long rendererPositionOffsetUs = loadingPeriodHolder == null ? periodStartPositionUs + : (loadingPeriodHolder.getRendererOffset() + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); + timeline.getPeriod(newLoadingPeriodIndex, period, true); + boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 + && !timeline.getWindow(period.windowIndex, window).isDynamic; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); - timeline.getWindow(windowIndex, window); - newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); + rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, + newLoadingPeriodIndex, isLastPeriod, periodStartPositionUs); if (loadingPeriodHolder != null) { - loadingPeriodHolder.setNext(newPeriodHolder); - newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - loadingPeriodHolder.startPositionUs; - } else { - newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; + loadingPeriodHolder.next = newPeriodHolder; } - bufferAheadPeriodCount++; loadingPeriodHolder = newPeriodHolder; + loadingPeriodHolder.mediaPeriod.prepare(this); setIsLoading(true); } } @@ -1168,13 +1148,12 @@ import java.io.IOException; } // Update the playing and reading periods. - while (playingPeriodHolder != readingPeriodHolder && playingPeriodHolder.next != null + while (playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); - bufferAheadPeriodCount--; playbackInfo = new PlaybackInfo(playingPeriodHolder.index, playingPeriodHolder.startPositionUs); updatePlaybackPositions(); @@ -1228,18 +1207,11 @@ import java.io.IOException; // Stale event. return; } - loadingPeriodHolder.handlePrepared(loadingPeriodHolder.startPositionUs, loadControl); + loadingPeriodHolder.handlePrepared(); if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; setPlayingPeriodHolder(readingPeriodHolder); - if (playbackInfo.startPositionUs == C.TIME_UNSET) { - // Update the playback info when seeking to a default position. - playbackInfo = new PlaybackInfo(playingPeriodHolder.index, - playingPeriodHolder.startPositionUs); - updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); - } updateTimelineState(); } maybeContinueLoading(); @@ -1247,6 +1219,7 @@ import java.io.IOException; private void handleContinueLoadingRequested(MediaPeriod period) { if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { + // Stale event. return; } maybeContinueLoading(); @@ -1366,22 +1339,22 @@ import java.io.IOException; public final MediaPeriod mediaPeriod; public final Object uid; - public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; + public final long rendererPositionOffsetUs; public int index; public long startPositionUs; public boolean isLast; public boolean prepared; public boolean hasEnabledTracks; - public long rendererPositionOffsetUs; public MediaPeriodHolder next; public boolean needsContinueLoading; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final LoadControl loadControl; private final MediaSource mediaSource; private Object trackSelectionsInfo; @@ -1390,17 +1363,23 @@ import java.io.IOException; private TrackSelectionArray periodTrackSelections; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - TrackSelector trackSelector, MediaSource mediaSource, MediaPeriod mediaPeriod, - Object uid, long positionUs) { + long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, + MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod, + long startPositionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; + this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; + this.loadControl = loadControl; this.mediaSource = mediaSource; - this.mediaPeriod = mediaPeriod; - this.uid = Assertions.checkNotNull(uid); + this.uid = Assertions.checkNotNull(periodUid); + this.index = periodIndex; + this.isLast = isLastPeriod; + this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; - startPositionUs = positionUs; + mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(), + startPositionUs); } public long toRendererTime(long periodTimeUs) { @@ -1415,13 +1394,9 @@ import java.io.IOException; return rendererPositionOffsetUs - startPositionUs; } - public void setNext(MediaPeriodHolder next) { - this.next = next; - } - - public void setIndex(Timeline timeline, Timeline.Window window, int periodIndex) { - this.index = periodIndex; - isLast = index == timeline.getPeriodCount() - 1 && !window.isDynamic; + public void setIndex(int index, boolean isLast) { + this.index = index; + this.isLast = isLast; } public boolean isFullyBuffered() { @@ -1429,12 +1404,11 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } - public void handlePrepared(long positionUs, LoadControl loadControl) - throws ExoPlaybackException { + public void handlePrepared() throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); selectTracks(); - startPositionUs = updatePeriodTrackSelection(positionUs, loadControl, false); + startPositionUs = updatePeriodTrackSelection(startPositionUs, false); } public boolean selectTracks() throws ExoPlaybackException { @@ -1449,14 +1423,13 @@ import java.io.IOException; return true; } - public long updatePeriodTrackSelection(long positionUs, LoadControl loadControl, - boolean forceRecreateStreams) throws ExoPlaybackException { - return updatePeriodTrackSelection(positionUs, loadControl, forceRecreateStreams, + public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) { + return updatePeriodTrackSelection(positionUs, forceRecreateStreams, new boolean[renderers.length]); } - public long updatePeriodTrackSelection(long positionUs, LoadControl loadControl, - boolean forceRecreateStreams, boolean[] streamResetFlags) { + public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams, + boolean[] streamResetFlags) { for (int i = 0; i < trackSelections.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), From ae0ac55b8db4d4aebbc15fa599c806d00e6b8be5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 21 Nov 2016 09:55:55 -0800 Subject: [PATCH 108/206] Add support for resetting the AudioTrack stream type. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139797714 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 6 +- .../ext/flac/LibflacAudioRenderer.java | 6 +- .../ext/opus/LibopusAudioRenderer.java | 13 ++--- .../java/com/google/android/exoplayer2/C.java | 57 ++++++++++++++++++- .../android/exoplayer2/SimpleExoPlayer.java | 36 +++++++++++- .../android/exoplayer2/audio/AudioTrack.java | 26 +++++++-- .../audio/MediaCodecAudioRenderer.java | 15 +++-- .../audio/SimpleDecoderAudioRenderer.java | 31 +++++----- 8 files changed, 144 insertions(+), 46 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 75d23dfa2e..1a70310a8d 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -20,7 +20,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -54,11 +53,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, int streamType) { - super(eventHandler, eventListener, audioCapabilities, streamType); + AudioCapabilities audioCapabilities) { + super(eventHandler, eventListener, audioCapabilities); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 0562851d3e..954a090ee9 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -19,7 +19,6 @@ import android.os.Handler; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -50,11 +49,10 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, int streamType) { - super(eventHandler, eventListener, audioCapabilities, streamType); + AudioCapabilities audioCapabilities) { + super(eventHandler, eventListener, audioCapabilities); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 60e5ff34b4..2dd2697aab 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -19,7 +19,6 @@ import android.os.Handler; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -52,11 +51,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, int streamType) { - super(eventHandler, eventListener, audioCapabilities, streamType); + AudioCapabilities audioCapabilities) { + super(eventHandler, eventListener, audioCapabilities); } /** @@ -65,12 +63,11 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, int streamType, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, streamType, drmSessionManager, + AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { + super(eventHandler, eventListener, audioCapabilities, drmSessionManager, playClearSamplesWithoutKeys); } diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 8c69524e95..3392aa64c6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.media.AudioFormat; +import android.media.AudioManager; import android.media.MediaCodec; import android.support.annotation.IntDef; import android.view.Surface; @@ -159,6 +160,42 @@ public final class C { public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; + /** + * Stream types for an {@link android.media.AudioTrack}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING, + STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL}) + public @interface StreamType {} + /** + * @see AudioManager#STREAM_ALARM + */ + public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; + /** + * @see AudioManager#STREAM_MUSIC + */ + public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC; + /** + * @see AudioManager#STREAM_NOTIFICATION + */ + public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION; + /** + * @see AudioManager#STREAM_RING + */ + public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING; + /** + * @see AudioManager#STREAM_SYSTEM + */ + public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM; + /** + * @see AudioManager#STREAM_VOICE_CALL + */ + public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; + /** + * The default stream type used by audio renderers. + */ + public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; + /** * Flags which can apply to a buffer containing a media sample. */ @@ -397,21 +434,35 @@ public final class C { public static final int MSG_SET_SURFACE = 1; /** - * The type of a message that can be passed to an audio {@link Renderer} via + * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * should be a {@link Float} with 0 being silence and 1 being unity gain. */ public static final int MSG_SET_VOLUME = 2; /** - * The type of a message that can be passed to an audio {@link Renderer} via + * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be a {@link android.media.PlaybackParams}, which will be used to configure the + * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the * underlying {@link android.media.AudioTrack}. The message object should not be modified by the * caller after it has been passed */ public static final int MSG_SET_PLAYBACK_PARAMS = 3; + /** + * A type of a message that can be passed to an audio {@link Renderer} via + * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object + * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream + * type of the underlying {@link android.media.AudioTrack}. See also + * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type + * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}. + *

    + * Note that when the stream type changes, the AudioTrack must be reinitialized, which can + * introduce a brief gap in audio output. Note also that tracks in the same audio session must + * share the same routing, so a new audio session id will be generated. + */ + public static final int MSG_SET_STREAM_TYPE = 4; + /** * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to * this value. diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ad12002b17..bb5f3d42ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2; import android.annotation.TargetApi; import android.content.Context; import android.graphics.SurfaceTexture; -import android.media.AudioManager; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; @@ -114,6 +113,8 @@ public final class SimpleExoPlayer implements ExoPlayer { private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; + @C.StreamType + private int audioStreamType; private float volume; private PlaybackParamsHolder playbackParamsHolder; @@ -152,6 +153,7 @@ public final class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioStreamType = C.STREAM_TYPE_DEFAULT; volume = 1; // Build the player and associated objects. @@ -232,6 +234,36 @@ public final class SimpleExoPlayer implements ExoPlayer { } } + /** + * Sets the stream type for audio playback (see {@link C.StreamType} and + * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type + * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}. + *

    + * Note that when the stream type changes, the AudioTrack must be reinitialized, which can + * introduce a brief gap in audio output. Note also that tracks in the same audio session must + * share the same routing, so a new audio session id will be generated. + * + * @param audioStreamType The stream type for audio playback. + */ + public void setAudioStreamType(@C.StreamType int audioStreamType) { + this.audioStreamType = audioStreamType; + ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; + int count = 0; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType); + } + } + player.sendMessages(messages); + } + + /** + * Returns the stream type for audio playback. + */ + public @C.StreamType int getAudioStreamType() { + return audioStreamType; + } + /** * Sets the audio volume, with 0 being silence and 1 being unity gain. * @@ -543,7 +575,7 @@ public final class SimpleExoPlayer implements ExoPlayer { Renderer audioRenderer = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, componentListener, - AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + AudioCapabilities.getCapabilities(context)); renderersList.add(audioRenderer); Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 41b0397b01..8e6cf68dc8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -249,7 +249,6 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; - private final int streamType; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -263,6 +262,8 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; + @C.StreamType + private int streamType; @C.Encoding private int sourceEncoding; @C.Encoding @@ -301,12 +302,10 @@ public final class AudioTrack { /** * @param audioCapabilities The current audio capabilities. - * @param streamType The type of audio stream for the underlying {@link android.media.AudioTrack}. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, int streamType, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { this.audioCapabilities = audioCapabilities; - this.streamType = streamType; this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -327,6 +326,7 @@ public final class AudioTrack { playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; + streamType = C.STREAM_TYPE_DEFAULT; } /** @@ -742,6 +742,24 @@ public final class AudioTrack { audioTrackUtil.setPlaybackParams(playbackParams); } + /** + * Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()} + * will return {@code false} and the caller must re-{@link #initialize(int)} the audio track + * before writing more data. The caller must not reuse the audio session identifier when + * re-initializing with a new stream type. + * + * @param streamType The {@link C.StreamType} to use for audio output. + * @return Whether the stream type changed. + */ + public boolean setStreamType(@C.StreamType int streamType) { + if (this.streamType == streamType) { + return false; + } + this.streamType = streamType; + reset(); + return true; + } + /** * Sets the playback volume. * diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index fb793c6a60..9d9601103e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.audio; import android.annotation.TargetApi; -import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; @@ -107,7 +106,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioRendererEventListener eventListener) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, null, AudioManager.STREAM_MUSIC); + eventListener, null); } /** @@ -124,16 +123,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - int streamType) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - audioTrack = new AudioTrack(audioCapabilities, streamType, this); + audioTrack = new AudioTrack(audioCapabilities, this); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -387,6 +384,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_PLAYBACK_PARAMS: audioTrack.setPlaybackParams((PlaybackParams) message); break; + case C.MSG_SET_STREAM_TYPE: + @C.StreamType int streamType = (Integer) message; + if (audioTrack.setStreamType(streamType)) { + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + } + break; default: super.handleMessage(messageType, message); break; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index c8bc9066f6..f8ba3c6ad8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.audio; -import android.media.AudioManager; import android.media.PlaybackParams; import android.os.Handler; import android.os.Looper; @@ -47,8 +46,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; - private final FormatHolder formatHolder; + private final AudioTrack audioTrack; private final DrmSessionManager drmSessionManager; + private final FormatHolder formatHolder; private DecoderCounters decoderCounters; private Format inputFormat; @@ -65,7 +65,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private boolean outputStreamEnded; private boolean waitingForKeys; - private final AudioTrack audioTrack; private int audioSessionId; public SimpleDecoderAudioRenderer() { @@ -79,7 +78,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null, AudioManager.STREAM_MUSIC); + this(eventHandler, eventListener, null); } /** @@ -88,12 +87,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - int streamType) { - this(eventHandler, eventListener, audioCapabilities, streamType, null, false); + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + this(eventHandler, eventListener, audioCapabilities, null, false); } /** @@ -102,7 +99,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param streamType The type of audio stream for the {@link AudioTrack}. * @param drmSessionManager For use with encrypted media. May be null if support for encrypted * media is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. @@ -113,15 +109,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - int streamType, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { super(C.TRACK_TYPE_AUDIO); - this.drmSessionManager = drmSessionManager; - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - audioTrack = new AudioTrack(audioCapabilities, streamType, this); + audioTrack = new AudioTrack(audioCapabilities, this); + this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; } @Override @@ -473,6 +468,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_PLAYBACK_PARAMS: audioTrack.setPlaybackParams((PlaybackParams) message); break; + case C.MSG_SET_STREAM_TYPE: + @C.StreamType int streamType = (Integer) message; + if (audioTrack.setStreamType(streamType)) { + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + } + break; default: super.handleMessage(messageType, message); break; From 4846146827a99eb99c529aeab34c81248ffa3c5d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Nov 2016 10:43:46 -0800 Subject: [PATCH 109/206] Fix internal build to use new Cronet jars ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139803684 --- extensions/cronet/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index ae2914dba3..eb15aadc65 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -42,7 +42,8 @@ android { dependencies { compile project(':library') compile files('libs/cronet_api.jar') - compile files('libs/cronet.jar') + compile files('libs/cronet_impl_common_java.jar') + compile files('libs/cronet_impl_native_java.jar') androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' From 9d7d8adc9c1cb9a683efac1ea06288fd393f9bfb Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Nov 2016 10:45:25 -0800 Subject: [PATCH 110/206] Allow changing of video scaling mode Issue #2016 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139803888 --- .../java/com/google/android/exoplayer2/C.java | 33 ++++++ .../android/exoplayer2/SimpleExoPlayer.java | 50 +++++++-- .../mediacodec/MediaCodecRenderer.java | 4 + .../video/MediaCodecVideoRenderer.java | 101 +++++++++--------- .../util/DebugMediaCodecVideoRenderer.java | 8 +- 5 files changed, 132 insertions(+), 64 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 3392aa64c6..3e6fac4a5e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -222,6 +222,29 @@ public final class C { */ public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000; + /** + * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}) + public @interface VideoScalingMode {} + /** + * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT + */ + @SuppressWarnings("InlinedApi") + public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; + /** + * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT + */ + @SuppressWarnings("InlinedApi") + public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; + /** + * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s. + */ + public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; + /** * Track selection flags. */ @@ -463,6 +486,16 @@ public final class C { */ public static final int MSG_SET_STREAM_TYPE = 4; + /** + * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} + * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message + * object should be one of the integer scaling modes in {@link C.VideoScalingMode}. + *

    + * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is + * owned by a {@link android.view.SurfaceView}. + */ + public static final int MSG_SET_SCALING_MODE = 5; + /** * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to * this value. diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index bb5f3d42ed..ca193c576b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -103,6 +103,8 @@ public final class SimpleExoPlayer implements ExoPlayer { private Surface surface; private boolean ownsSurface; + @C.VideoScalingMode + private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; private TextRenderer.Output textOutput; @@ -115,7 +117,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private int audioSessionId; @C.StreamType private int audioStreamType; - private float volume; + private float audioVolume; private PlaybackParamsHolder playbackParamsHolder; /* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector, @@ -152,14 +154,43 @@ public final class SimpleExoPlayer implements ExoPlayer { this.audioRendererCount = audioRendererCount; // Set initial values. + audioVolume = 1; audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioStreamType = C.STREAM_TYPE_DEFAULT; - volume = 1; + videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. player = new ExoPlayerImpl(renderers, trackSelector, loadControl); } + /** + * Sets the video scaling mode. + *

    + * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is + * enabled and if the output surface is owned by a {@link android.view.SurfaceView}. + * + * @param videoScalingMode The video scaling mode. + */ + public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { + this.videoScalingMode = videoScalingMode; + ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; + int count = 0; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, + videoScalingMode); + } + } + player.sendMessages(messages); + } + + /** + * Returns the video scaling mode. + */ + public @C.VideoScalingMode int getVideoScalingMode() { + return videoScalingMode; + } + /** * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} * currently set on the player. @@ -267,15 +298,15 @@ public final class SimpleExoPlayer implements ExoPlayer { /** * Sets the audio volume, with 0 being silence and 1 being unity gain. * - * @param volume The volume. + * @param audioVolume The audio volume. */ - public void setVolume(float volume) { - this.volume = volume; + public void setVolume(float audioVolume) { + this.audioVolume = audioVolume; ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, volume); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); } } player.sendMessages(messages); @@ -285,7 +316,7 @@ public final class SimpleExoPlayer implements ExoPlayer { * Returns the audio volume, with 0 being silence and 1 being unity gain. */ public float getVolume() { - return volume; + return audioVolume; } /** @@ -568,9 +599,8 @@ public final class SimpleExoPlayer implements ExoPlayer { DrmSessionManager drmSessionManager, ArrayList renderersList, long allowedVideoJoiningTimeMs) { MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer(context, - MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, - allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, componentListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + MediaCodecSelector.DEFAULT, allowedVideoJoiningTimeMs, drmSessionManager, false, + mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); renderersList.add(videoRenderer); Renderer audioRenderer = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 63a77e2215..ca06a00619 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -375,6 +375,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return codec == null && format != null; } + protected final MediaCodec getCodec() { + return codec; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index b94beb10f6..c2d5558225 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -57,7 +57,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; - private final int videoScalingMode; private final int maxDroppedFramesToNotify; private final boolean deviceNeedsAutoFrcWorkaround; @@ -65,6 +64,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private CodecMaxValues codecMaxValues; private Surface surface; + @C.VideoScalingMode + private int scalingMode; private boolean renderedFirstFrame; private long joiningDeadlineMs; private long droppedFrameAccumulationStartTimeMs; @@ -85,32 +86,25 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. */ - public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - int videoScalingMode) { - this(context, mediaCodecSelector, videoScalingMode, 0); + public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) { + this(context, mediaCodecSelector, 0); } /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - int videoScalingMode, long allowedJoiningTimeMs) { - this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, -1); + long allowedJoiningTimeMs) { + this(context, mediaCodecSelector, allowedJoiningTimeMs, null, null, -1); } /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be @@ -120,17 +114,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler, - VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { - this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, - eventHandler, eventListener, maxDroppedFrameCountToNotify); + long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, + int maxDroppedFrameCountToNotify) { + this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, + eventListener, maxDroppedFrameCountToNotify); } /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted @@ -147,12 +139,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - int videoScalingMode, long allowedJoiningTimeMs, - DrmSessionManager drmSessionManager, + long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - this.videoScalingMode = videoScalingMode; this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); @@ -163,6 +153,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; + scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; clearLastReportedVideoSize(); } @@ -284,6 +275,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { setSurface((Surface) message); + } else if (messageType == C.MSG_SET_SCALING_MODE) { + scalingMode = (Integer) message; + MediaCodec codec = getCodec(); + if (codec != null) { + setVideoScalingMode(codec, scalingMode); + } } else { super.handleMessage(messageType, message); } @@ -358,7 +355,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentUnappliedRotationDegrees = pendingRotationDegrees; } // Must be applied each time the output format changes. - codec.setVideoScalingMode(videoScalingMode); + setVideoScalingMode(codec, scalingMode); } @Override @@ -488,6 +485,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + private void clearLastReportedVideoSize() { + lastReportedWidth = Format.NO_VALUE; + lastReportedHeight = Format.NO_VALUE; + lastReportedPixelWidthHeightRatio = Format.NO_VALUE; + lastReportedUnappliedRotationDegrees = Format.NO_VALUE; + } + + private void maybeNotifyVideoSizeChanged() { + if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight + || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees + || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, + currentPixelWidthHeightRatio); + lastReportedWidth = currentWidth; + lastReportedHeight = currentHeight; + lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; + lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + } + } + + private void maybeNotifyDroppedFrames() { + if (droppedFrames > 0) { + long now = SystemClock.elapsedRealtime(); + long elapsedMs = now - droppedFrameAccumulationStartTimeMs; + eventDispatcher.droppedFrames(droppedFrames, elapsedMs); + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = now; + } + } + @SuppressLint("InlinedApi") private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, boolean deviceNeedsAutoFrcWorkaround) { @@ -583,34 +610,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return (maxPixels * 3) / (2 * minCompressionRatio); } - private void clearLastReportedVideoSize() { - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; - lastReportedPixelWidthHeightRatio = Format.NO_VALUE; - lastReportedUnappliedRotationDegrees = Format.NO_VALUE; - } - - private void maybeNotifyVideoSizeChanged() { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight - || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { - eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, - currentPixelWidthHeightRatio); - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; - } - } - - private void maybeNotifyDroppedFrames() { - if (droppedFrames > 0) { - long now = SystemClock.elapsedRealtime(); - long elapsedMs = now - droppedFrameAccumulationStartTimeMs; - eventDispatcher.droppedFrames(droppedFrames, elapsedMs); - droppedFrames = 0; - droppedFrameAccumulationStartTimeMs = now; - } + private static void setVideoScalingMode(MediaCodec codec, int scalingMode) { + codec.setVideoScalingMode(scalingMode); } /** diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java index cbc5f35e94..e38dea948a 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java @@ -40,10 +40,10 @@ public class DebugMediaCodecVideoRenderer extends MediaCodecVideoRenderer { private int bufferCount; public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler, - VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { - super(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, - eventHandler, eventListener, maxDroppedFrameCountToNotify); + long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, + int maxDroppedFrameCountToNotify) { + super(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, + eventListener, maxDroppedFrameCountToNotify); startIndex = 0; queueSize = 0; } From 4f3ab7b22d0a8863907012da4be84fb705f2ed3a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Nov 2016 10:50:30 -0800 Subject: [PATCH 111/206] ExoPlayerImplInternal: Some more minor cleanup - This should be a no-op change - Inline attemptRestart - Clean up processing of pending seeks ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139804630 --- .../exoplayer2/ExoPlayerImplInternal.java | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f8cacb43a7..2b71163c6d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -839,17 +839,21 @@ import java.io.IOException; timeline = timelineAndManifest.first; Object manifest = timelineAndManifest.second; + int processedInitialSeekCount = 0; if (oldTimeline == null) { if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); if (periodPosition == null) { // We failed to resolve the seek position. Stop the player. - finishSourceInfoRefresh(manifest, false); + notifySourceInfoRefresh(manifest, 0); // TODO: We should probably propagate an error here. stopInternal(); return; } playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); + processedInitialSeekCount = pendingInitialSeekCount; + pendingInitialSeekCount = 0; + pendingSeekPosition = null; } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); @@ -860,19 +864,34 @@ import java.io.IOException; : loadingPeriodHolder; if (periodHolder == null) { // We don't have any period holders, so we're done. - finishSourceInfoRefresh(manifest, true); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); if (periodIndex == C.INDEX_UNSET) { - // We didn't find the current period in the new timeline. Attempt to restart. - boolean restarted = attemptRestart(periodHolder, oldTimeline, timeline); - finishSourceInfoRefresh(manifest, true); - if (!restarted) { + // We didn't find the current period in the new timeline. Attempt to resolve a subsequent + // period whose window we can restart from. + int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); + if (newPeriodIndex == C.INDEX_UNSET) { + // We failed to resolve a subsequent period. Stop the player. + notifySourceInfoRefresh(manifest, processedInitialSeekCount); // TODO: We should probably propagate an error here. stopInternal(); + return; } + // Release all loaded periods. + releasePeriodHoldersFrom(periodHolder); + playingPeriodHolder = null; + readingPeriodHolder = null; + loadingPeriodHolder = null; + // Find the default initial position in the window and seek to it. + Pair defaultPosition = getPeriodPosition( + timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); + newPeriodIndex = defaultPosition.first; + long newPlayingPositionUs = defaultPosition.second; + playbackInfo = new PlaybackInfo(newPeriodIndex, newPlayingPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } @@ -925,40 +944,12 @@ import java.io.IOException; } } - finishSourceInfoRefresh(manifest, true); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - private boolean attemptRestart(MediaPeriodHolder oldPeriodHolder, Timeline oldTimeline, - Timeline newTimeline) { - int newPeriodIndex = resolveSubsequentPeriod(oldPeriodHolder.index, oldTimeline, newTimeline); - if (newPeriodIndex == C.INDEX_UNSET) { - // We failed to find a replacement period. Stop the player. - return false; - } - - // Release all loaded periods. - releasePeriodHoldersFrom(oldPeriodHolder); - playingPeriodHolder = null; - readingPeriodHolder = null; - loadingPeriodHolder = null; - - // Find the default initial position in the window and seek to it. - Pair defaultPosition = getPeriodPosition( - timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); - newPeriodIndex = defaultPosition.first; - long newPlayingPositionUs = defaultPosition.second; - playbackInfo = new PlaybackInfo(newPeriodIndex, newPlayingPositionUs); - return true; - } - - private void finishSourceInfoRefresh(Object manifest, boolean processedInitialSeeks) { - SourceInfo sourceInfo = new SourceInfo(timeline, manifest, playbackInfo, - processedInitialSeeks ? pendingInitialSeekCount : 0); - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, sourceInfo).sendToTarget(); - if (processedInitialSeeks) { - pendingInitialSeekCount = 0; - pendingSeekPosition = null; - } + private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, + new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); } /** From 971bf768ffbcab5d39a4e40c89b6c7b16bc69e3e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 24 Nov 2016 19:29:15 +0000 Subject: [PATCH 112/206] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 3667c8cc96..7e9f2a059d 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,3 +1,5 @@ +*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE *** + Please search the existing issues before filing a new one, including issues that are closed. When filing a new issue please include all of the following, unless you're certain that they're not useful for the particular issue being reported. From 90e0919d09a9dd85ab3a03a74091d43a35ad360e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 24 Nov 2016 19:30:15 +0000 Subject: [PATCH 113/206] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 7e9f2a059d..6e55f3dcd6 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,7 +1,7 @@ *** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE *** Please search the existing issues before filing a new one, including issues that -are closed. When filing a new issue please include all of the following, unless +are closed. When filing a new issue please include ALL of the following, unless you're certain that they're not useful for the particular issue being reported. - A description of the issue. From 81ce6bba7a69063a6c8059b0597873cddc06dc0e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Nov 2016 11:51:59 -0800 Subject: [PATCH 114/206] Allow DashMediaSource to take an optional DashManifest Also allow custom DashManifestParser injection, to support parsing of custom elements and attributes that service providers may wish to include in their manifests (e.g. #2058). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139813108 --- .../source/dash/DashMediaSource.java | 184 ++++++++++++++---- .../upstream/LoaderErrorThrower.java | 17 ++ 2 files changed, 167 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index e6bf8b5c02..631a44f859 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -80,6 +81,7 @@ public final class DashMediaSource implements MediaSource { private static final String TAG = "DashMediaSource"; + private final boolean sideloadedManifest; private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; @@ -95,6 +97,7 @@ public final class DashMediaSource implements MediaSource { private MediaSource.Listener sourceListener; private DataSource dataSource; private Loader loader; + private LoaderErrorThrower loaderErrorThrower; private Uri manifestUri; private long manifestLoadStartTimestamp; @@ -105,6 +108,47 @@ public final class DashMediaSource implements MediaSource { private int firstPeriodId; + /** + * Constructs an instance to play a given {@link DashManifest}, which must be static. + * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, + Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + eventListener); + } + + /** + * Constructs an instance to play a given {@link DashManifest}, which must be static. + * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener + eventListener) { + this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); + } + + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or + * static. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -113,32 +157,91 @@ public final class DashMediaSource implements MediaSource { eventHandler, eventListener); } + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or + * static. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. Use + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by + * the manifest, if present. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, + minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + } + + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or + * static. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param manifestParser A parser for loaded manifest data. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. Use + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by + * the manifest, if present. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + DashManifestParser manifestParser, DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, + minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + } + + private DashMediaSource(DashManifest manifest, Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, DashManifestParser manifestParser, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.manifest = manifest; this.manifestUri = manifestUri; this.manifestDataSourceFactory = manifestDataSourceFactory; + this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; + sideloadedManifest = manifest != null; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - manifestParser = new DashManifestParser(); - manifestCallback = new ManifestCallback(); manifestUriLock = new Object(); periodsById = new SparseArray<>(); - refreshManifestRunnable = new Runnable() { - @Override - public void run() { - startLoadingManifest(); - } - }; - simulateManifestRefreshRunnable = new Runnable() { - @Override - public void run() { - processManifest(); - } - }; + if (sideloadedManifest) { + Assertions.checkState(!manifest.dynamic); + manifestCallback = null; + refreshManifestRunnable = null; + simulateManifestRefreshRunnable = null; + } else { + manifestCallback = new ManifestCallback(); + refreshManifestRunnable = new Runnable() { + @Override + public void run() { + startLoadingManifest(); + } + }; + simulateManifestRefreshRunnable = new Runnable() { + @Override + public void run() { + processManifest(false); + } + }; + } } /** @@ -157,15 +260,21 @@ public final class DashMediaSource implements MediaSource { @Override public void prepareSource(MediaSource.Listener listener) { sourceListener = listener; - dataSource = manifestDataSourceFactory.createDataSource(); - loader = new Loader("Loader:DashMediaSource"); - handler = new Handler(); - startLoadingManifest(); + if (sideloadedManifest) { + loaderErrorThrower = new LoaderErrorThrower.Dummy(); + processManifest(false); + } else { + dataSource = manifestDataSourceFactory.createDataSource(); + loader = new Loader("Loader:DashMediaSource"); + loaderErrorThrower = loader; + handler = new Handler(); + startLoadingManifest(); + } } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - loader.maybeThrowError(); + loaderErrorThrower.maybeThrowError(); } @Override @@ -174,7 +283,7 @@ public final class DashMediaSource implements MediaSource { manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, - elapsedRealtimeOffsetMs, loader, allocator); + elapsedRealtimeOffsetMs, loaderErrorThrower, allocator); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } @@ -189,6 +298,7 @@ public final class DashMediaSource implements MediaSource { @Override public void releaseSource() { dataSource = null; + loaderErrorThrower = null; if (loader != null) { loader.release(); loader = null; @@ -247,11 +357,11 @@ public final class DashMediaSource implements MediaSource { if (manifest.utcTiming != null) { resolveUtcTimingElement(manifest.utcTiming); } else { - processManifestAndScheduleRefresh(); + processManifest(true); } } else { firstPeriodId += removedPeriodCount; - processManifestAndScheduleRefresh(); + processManifest(true); } } @@ -327,21 +437,16 @@ public final class DashMediaSource implements MediaSource { private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; - processManifestAndScheduleRefresh(); + processManifest(true); } private void onUtcTimestampResolutionError(IOException error) { Log.e(TAG, "Failed to resolve UtcTiming element.", error); // Be optimistic and continue in the hope that the device clock is correct. - processManifestAndScheduleRefresh(); + processManifest(true); } - private void processManifestAndScheduleRefresh() { - processManifest(); - scheduleManifestRefresh(); - } - - private void processManifest() { + private void processManifest(boolean scheduleRefresh) { // Update any periods. for (int i = 0; i < periodsById.size(); i++) { int id = periodsById.keyAt(i); @@ -351,9 +456,8 @@ public final class DashMediaSource implements MediaSource { // This period has been removed from the manifest so it doesn't need to be updated. } } - // Remove any pending simulated updates. - handler.removeCallbacks(simulateManifestRefreshRunnable); // Update the window. + boolean windowChangingImplicitly = false; int lastPeriodIndex = manifest.getPeriodCount() - 1; PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0), manifest.getPeriodDurationUs(0)); @@ -384,8 +488,7 @@ public final class DashMediaSource implements MediaSource { currentStartTimeUs = manifest.getPeriodDurationUs(0); } } - // The window is changing implicitly. Post a simulated manifest refresh to update it. - handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); + windowChangingImplicitly = true; } long windowDurationUs = currentEndTimeUs - currentStartTimeUs; for (int i = 0; i < manifest.getPeriodCount() - 1; i++) { @@ -414,6 +517,19 @@ public final class DashMediaSource implements MediaSource { firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, manifest); sourceListener.onSourceInfoRefreshed(timeline, manifest); + + if (!sideloadedManifest) { + // Remove any pending simulated refresh. + handler.removeCallbacks(simulateManifestRefreshRunnable); + // If the window is changing implicitly, post a simulated manifest refresh to update it. + if (windowChangingImplicitly) { + handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); + } + // Schedule an explicit refresh if needed. + if (scheduleRefresh) { + scheduleManifestRefresh(); + } + } } private void scheduleManifestRefresh() { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java index e5d62378b3..4f9e9fa5e6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java @@ -43,4 +43,21 @@ public interface LoaderErrorThrower { */ void maybeThrowError(int minRetryCount) throws IOException; + /** + * A {@link LoaderErrorThrower} that never throws. + */ + final class Dummy implements LoaderErrorThrower { + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public void maybeThrowError(int minRetryCount) throws IOException { + // Do nothing. + } + + } + } From 42fadfe083335f1de0f342e68dc5469c3ed41b39 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Nov 2016 11:56:38 -0800 Subject: [PATCH 115/206] Allow SsMediaSource to take an optional SsManifest Also allow custom SsManifestParser injection. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139813660 --- .../source/smoothstreaming/SsMediaSource.java | 164 ++++++++++++++---- 1 file changed, 133 insertions(+), 31 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index d328e5ecf2..69f32be193 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -64,7 +65,7 @@ public final class SsMediaSource implements MediaSource, private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; private final Uri manifestUri; - private final DataSource.Factory dataSourceFactory; + private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; @@ -75,12 +76,54 @@ public final class SsMediaSource implements MediaSource, private MediaSource.Listener sourceListener; private DataSource manifestDataSource; private Loader manifestLoader; + private LoaderErrorThrower manifestLoaderErrorThrower; private long manifestLoadStartTimestamp; private SsManifest manifest; private Handler manifestRefreshHandler; + /** + * Constructs an instance to play a given {@link SsManifest}, which must not be live. + * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, + Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, + eventHandler, eventListener); + } + + /** + * Constructs an instance to play a given {@link SsManifest}, which must not be live. + * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); + } + + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or + * on-demand. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -89,18 +132,67 @@ public final class SsMediaSource implements MediaSource, eventListener); } - public SsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or + * on-demand. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this.manifestUri = Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") - ? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest"); - this.dataSourceFactory = dataSourceFactory; + this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, + minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + } + + /** + * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or + * on-demand. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param manifestParser A parser for loaded manifest data. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + SsManifestParser manifestParser, SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, + minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + } + + private SsMediaSource(SsManifest manifest, Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, SsManifestParser manifestParser, + SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + Assertions.checkState(manifest == null || !manifest.isLive); + this.manifest = manifest; + this.manifestUri = manifestUri == null ? null + : Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") ? manifestUri + : Uri.withAppendedPath(manifestUri, "Manifest"); + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); - manifestParser = new SsManifestParser(); mediaPeriods = new ArrayList<>(); } @@ -109,22 +201,28 @@ public final class SsMediaSource implements MediaSource, @Override public void prepareSource(MediaSource.Listener listener) { sourceListener = listener; - manifestDataSource = dataSourceFactory.createDataSource(); - manifestLoader = new Loader("Loader:Manifest"); - manifestRefreshHandler = new Handler(); - startLoadingManifest(); + if (manifest != null) { + manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); + processManifest(); + } else { + manifestDataSource = manifestDataSourceFactory.createDataSource(); + manifestLoader = new Loader("Loader:Manifest"); + manifestLoaderErrorThrower = manifestLoader; + manifestRefreshHandler = new Handler(); + startLoadingManifest(); + } } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - manifestLoader.maybeThrowError(); + manifestLoaderErrorThrower.maybeThrowError(); } @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { Assertions.checkArgument(index == 0); SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, - eventDispatcher, manifestLoader, allocator); + eventDispatcher, manifestLoaderErrorThrower, allocator); mediaPeriods.add(period); return period; } @@ -160,6 +258,29 @@ public final class SsMediaSource implements MediaSource, loadDurationMs, loadable.bytesLoaded()); manifest = loadable.getResult(); manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; + processManifest(); + scheduleManifestRefresh(); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods + + private void processManifest() { for (int i = 0; i < mediaPeriods.size(); i++) { mediaPeriods.get(i).updateManifest(manifest); } @@ -198,27 +319,8 @@ public final class SsMediaSource implements MediaSource, timeline = new SinglePeriodTimeline(manifest.durationUs, isSeekable); } sourceListener.onSourceInfoRefreshed(timeline, manifest); - scheduleManifestRefresh(); } - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // Internal methods - private void scheduleManifestRefresh() { if (!manifest.isLive) { return; From e84fa5835d0e6595646ded0857d59ba2dab4029c Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 8 Nov 2016 03:27:52 +0000 Subject: [PATCH 116/206] Use ReusableBufferedOutputStream for cache index file write operation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139889957 --- .../upstream/cache/CachedContentIndex.java | 14 ++++++++++---- .../util/ReusableBufferedOutputStream.java | 10 ++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 64863ac42b..d8224665b9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -20,9 +20,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.ReusableBufferedOutputStream; import com.google.android.exoplayer2.util.Util; import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -61,6 +61,7 @@ import javax.crypto.spec.SecretKeySpec; private final Cipher cipher; private final SecretKeySpec secretKeySpec; private boolean changed; + private ReusableBufferedOutputStream bufferedOutputStream; /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ public CachedContentIndex(File cacheDir) { @@ -256,8 +257,13 @@ import javax.crypto.spec.SecretKeySpec; private void writeFile() throws CacheException { DataOutputStream output = null; try { - OutputStream outputStream = new BufferedOutputStream(atomicFile.startWrite()); - output = new DataOutputStream(outputStream); + OutputStream outputStream = atomicFile.startWrite(); + if (bufferedOutputStream == null) { + bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); + } else { + bufferedOutputStream.reset(outputStream); + } + output = new DataOutputStream(bufferedOutputStream); output.writeInt(VERSION); int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; @@ -274,7 +280,7 @@ import javax.crypto.spec.SecretKeySpec; throw new IllegalStateException(e); // Should never happen. } output.flush(); - output = new DataOutputStream(new CipherOutputStream(outputStream, cipher)); + output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); } output.writeInt(keyToContent.size()); diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java index 1ae947b610..a3d1d4d02e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java @@ -25,6 +25,8 @@ import java.io.OutputStream; */ public final class ReusableBufferedOutputStream extends BufferedOutputStream { + private boolean closed; + public ReusableBufferedOutputStream(OutputStream out) { super(out); } @@ -35,13 +37,14 @@ public final class ReusableBufferedOutputStream extends BufferedOutputStream { @Override public void close() throws IOException { + closed = true; + Throwable thrown = null; try { flush(); } catch (Throwable e) { thrown = e; } - try { out.close(); } catch (Throwable e) { @@ -49,8 +52,6 @@ public final class ReusableBufferedOutputStream extends BufferedOutputStream { thrown = e; } } - out = null; - if (thrown != null) { Util.sneakyThrow(thrown); } @@ -64,7 +65,8 @@ public final class ReusableBufferedOutputStream extends BufferedOutputStream { * @throws IllegalStateException If the stream isn't closed. */ public void reset(OutputStream out) { - Assertions.checkState(this.out == null); + Assertions.checkState(closed); this.out = out; + closed = false; } } From 77715fbfbe715721d48a0483cf045fa258118c80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 Nov 2016 06:30:14 -0800 Subject: [PATCH 117/206] Fix some analysis warnings. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139901449 --- extensions/cronet/build.gradle | 2 +- library/build.gradle | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 4 +- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 2 +- .../exoplayer2/extractor/ogg/OggTestFile.java | 18 +-- .../extractor/ts/TsExtractorTest.java | 3 +- .../text/webvtt/Mp4WebvttDecoderTest.java | 2 +- .../cache/CachedContentIndexTest.java | 2 +- .../exoplayer2/util/AtomicFileTest.java | 2 + .../drm/StreamingDrmSessionManager.java | 10 +- .../extractor/mkv/MatroskaExtractor.java | 124 +++++++++--------- .../exoplayer2/text/cea/Cea608Decoder.java | 10 +- .../exoplayer2/upstream/ParsingLoadable.java | 2 +- .../upstream/cache/SimpleCache.java | 5 +- .../android/exoplayer2/util/AtomicFile.java | 2 +- 15 files changed, 94 insertions(+), 96 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index eb15aadc65..3c9a36c891 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -48,5 +48,5 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' androidTestCompile project(':library') - androidTestCompile 'com.android.support.test:runner:0.4' + androidTestCompile 'com.android.support.test:runner:0.5' } diff --git a/library/build.gradle b/library/build.gradle index dd1d5c3c87..5ec947d0eb 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -57,7 +57,7 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' - compile 'com.android.support:support-annotations:24.2.1' + compile 'com.android.support:support-annotations:25.0.1' } android.libraryVariants.all { variant -> diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index 71edba0612..cb1751d43b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -110,8 +110,8 @@ public final class DefaultOggSeekerTest extends TestCase { long granuleDiff = currentGranule - targetGranule; if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { - fail(String.format("granuleDiff (%d) or positionDiff (%d) is more than allowed.", - granuleDiff, positionDiff)); + fail("granuleDiff (" + granuleDiff + ") or positionDiff (" + positionDiff + + ") is more than allowed."); } } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index 5431d35bdf..d52deb108f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -28,7 +28,7 @@ import junit.framework.TestCase; */ public class DefaultOggSeekerUtilMethodsTest extends TestCase { - private Random random = new Random(0); + private final Random random = new Random(0); public void testSkipToNextPage() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index b1294c7a14..d5d187ee7c 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -25,16 +25,16 @@ import junit.framework.Assert; */ /* package */ final class OggTestFile { - public static final int MAX_PACKET_LENGTH = 2048; - public static final int MAX_SEGMENT_COUNT = 10; - public static final int MAX_GRANULES_IN_PAGE = 100000; + private static final int MAX_PACKET_LENGTH = 2048; + private static final int MAX_SEGMENT_COUNT = 10; + private static final int MAX_GRANULES_IN_PAGE = 100000; - byte[] data; - long lastGranule; - int packetCount; - int pageCount; - int firstPayloadPageSize; - long firstPayloadPageGranulePosition; + public final byte[] data; + public final long lastGranule; + public final int packetCount; + public final int pageCount; + public final int firstPayloadPageSize; + public final long firstPayloadPageGranulePosition; private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount, int firstPayloadPageSize, long firstPayloadPageGranulePosition) { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 58893f15c1..c9d6535164 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.util.Random; /** @@ -114,7 +113,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { assertEquals(1, factory.sdtReader.consumedSdts); } - private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException { + private static void writeJunkData(ByteArrayOutputStream out, int length) { for (int i = 0; i < length; i++) { if (((byte) i) == TS_SYNC_BYTE) { out.write(0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 9f52453908..a0feaea57d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -97,7 +97,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { public void testNoCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length); - assertMp4WebvttSubtitleEquals(result, new Cue[0]); + assertMp4WebvttSubtitleEquals(result); } // Negative tests. diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 4e9171c53b..dd4de7cce2 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -28,7 +28,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length 0, 0, 0, 2, // cache_id 0, 5, 75, 76, 77, 78, 79, // cache_key - 0, 0, 0, 0, 0, 0, 10, 00, // original_content_length + 0, 0, 0, 0, 0, 0, 10, 0, // original_content_length (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array }; private CachedContentIndex index; diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index afe28a1e99..cbe4acbae5 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -69,10 +69,12 @@ public class AtomicFileTest extends InstrumentationTestCase { output.write(6); assertRead(); + output.close(); output = atomicFile.startWrite(); assertRead(); + output.close(); } private void assertRead() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java index f3c6595736..4e4845c70b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java @@ -447,15 +447,15 @@ public class StreamingDrmSessionManager implements Drm switch (msg.what) { case MediaDrm.EVENT_KEY_REQUIRED: postKeyRequest(); - return; + break; case MediaDrm.EVENT_KEY_EXPIRED: state = STATE_OPENED; onError(new KeysExpiredException()); - return; + break; case MediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; postProvisionRequest(); - return; + break; } } @@ -483,10 +483,10 @@ public class StreamingDrmSessionManager implements Drm switch (msg.what) { case MSG_PROVISION: onProvisionResponse(msg.obj); - return; + break; case MSG_KEYS: onKeyResponse(msg.obj); - return; + break; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index bc2d891dab..301392b2d2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -431,18 +431,18 @@ public final class MatroskaExtractor implements Extractor { } segmentContentPosition = contentPosition; segmentContentSize = contentSize; - return; + break; case ID_SEEK: seekEntryId = UNSET_ENTRY_ID; seekEntryPosition = C.POSITION_UNSET; - return; + break; case ID_CUES: cueTimesUs = new LongArray(); cueClusterPositions = new LongArray(); - return; + break; case ID_CUE_POINT: seenClusterPositionForCurrentCuePoint = false; - return; + break; case ID_CLUSTER: if (!sentSeekMap) { // We need to build cues before parsing the cluster. @@ -456,21 +456,21 @@ public final class MatroskaExtractor implements Extractor { sentSeekMap = true; } } - return; + break; case ID_BLOCK_GROUP: sampleSeenReferenceBlock = false; - return; + break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. - return; + break; case ID_CONTENT_ENCRYPTION: currentTrack.hasContentEncryption = true; - return; + break; case ID_TRACK_ENTRY: currentTrack = new Track(); - return; + break; default: - return; + break; } } @@ -484,7 +484,7 @@ public final class MatroskaExtractor implements Extractor { if (durationTimecode != C.TIME_UNSET) { durationUs = scaleTimecodeToUs(durationTimecode); } - return; + break; case ID_SEEK: if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) { throw new ParserException("Mandatory element SeekID or SeekPosition not found"); @@ -492,7 +492,7 @@ public final class MatroskaExtractor implements Extractor { if (seekEntryId == ID_CUES) { cuesContentPosition = seekEntryPosition; } - return; + break; case ID_CUES: if (!sentSeekMap) { extractorOutput.seekMap(buildSeekMap()); @@ -500,7 +500,7 @@ public final class MatroskaExtractor implements Extractor { } else { // We have already built the cues. Ignore. } - return; + break; case ID_BLOCK_GROUP: if (blockState != BLOCK_STATE_DATA) { // We've skipped this block (due to incompatible track number). @@ -512,7 +512,7 @@ public final class MatroskaExtractor implements Extractor { } commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); blockState = BLOCK_STATE_START; - return; + break; case ID_CONTENT_ENCODING: if (currentTrack.hasContentEncryption) { if (currentTrack.encryptionKeyId == null) { @@ -521,12 +521,12 @@ public final class MatroskaExtractor implements Extractor { currentTrack.drmInitData = new DrmInitData( new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); } - return; + break; case ID_CONTENT_ENCODINGS: if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) { throw new ParserException("Combining encryption and compression is not supported"); } - return; + break; case ID_TRACK_ENTRY: if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) { currentTrack.initializeOutput(extractorOutput, currentTrack.number); @@ -535,15 +535,15 @@ public final class MatroskaExtractor implements Extractor { // We've seen this track entry before, or the codec is unsupported. Do nothing. } currentTrack = null; - return; + break; case ID_TRACKS: if (tracks.size() == 0) { throw new ParserException("No valid tracks were found"); } extractorOutput.endTracks(); - return; + break; default: - return; + break; } } @@ -554,99 +554,99 @@ public final class MatroskaExtractor implements Extractor { if (value != 1) { throw new ParserException("EBMLReadVersion " + value + " not supported"); } - return; + break; case ID_DOC_TYPE_READ_VERSION: // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. if (value < 1 || value > 2) { throw new ParserException("DocTypeReadVersion " + value + " not supported"); } - return; + break; case ID_SEEK_POSITION: // Seek Position is the relative offset beginning from the Segment. So to get absolute // offset from the beginning of the file, we need to add segmentContentPosition to it. seekEntryPosition = value + segmentContentPosition; - return; + break; case ID_TIMECODE_SCALE: timecodeScale = value; - return; + break; case ID_PIXEL_WIDTH: currentTrack.width = (int) value; - return; + break; case ID_PIXEL_HEIGHT: currentTrack.height = (int) value; - return; + break; case ID_DISPLAY_WIDTH: currentTrack.displayWidth = (int) value; - return; + break; case ID_DISPLAY_HEIGHT: currentTrack.displayHeight = (int) value; - return; + break; case ID_DISPLAY_UNIT: currentTrack.displayUnit = (int) value; - return; + break; case ID_TRACK_NUMBER: currentTrack.number = (int) value; - return; + break; case ID_FLAG_DEFAULT: currentTrack.flagForced = value == 1; - return; + break; case ID_FLAG_FORCED: currentTrack.flagDefault = value == 1; - return; + break; case ID_TRACK_TYPE: currentTrack.type = (int) value; - return; + break; case ID_DEFAULT_DURATION: currentTrack.defaultSampleDurationNs = (int) value; - return; + break; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; - return; + break; case ID_SEEK_PRE_ROLL: currentTrack.seekPreRollNs = value; - return; + break; case ID_CHANNELS: currentTrack.channelCount = (int) value; - return; + break; case ID_AUDIO_BIT_DEPTH: currentTrack.audioBitDepth = (int) value; - return; + break; case ID_REFERENCE_BLOCK: sampleSeenReferenceBlock = true; - return; + break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. if (value != 0) { throw new ParserException("ContentEncodingOrder " + value + " not supported"); } - return; + break; case ID_CONTENT_ENCODING_SCOPE: // This extractor only supports the scope of all frames. if (value != 1) { throw new ParserException("ContentEncodingScope " + value + " not supported"); } - return; + break; case ID_CONTENT_COMPRESSION_ALGORITHM: // This extractor only supports header stripping. if (value != 3) { throw new ParserException("ContentCompAlgo " + value + " not supported"); } - return; + break; case ID_CONTENT_ENCRYPTION_ALGORITHM: // Only the value 5 (AES) is allowed according to the WebM specification. if (value != 5) { throw new ParserException("ContentEncAlgo " + value + " not supported"); } - return; + break; case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: // Only the value 1 is allowed according to the WebM specification. if (value != 1) { throw new ParserException("AESSettingsCipherMode " + value + " not supported"); } - return; + break; case ID_CUE_TIME: cueTimesUs.add(scaleTimecodeToUs(value)); - return; + break; case ID_CUE_CLUSTER_POSITION: if (!seenClusterPositionForCurrentCuePoint) { // If there's more than one video/audio track, then there could be more than one @@ -655,13 +655,13 @@ public final class MatroskaExtractor implements Extractor { cueClusterPositions.add(value); seenClusterPositionForCurrentCuePoint = true; } - return; + break; case ID_TIME_CODE: clusterTimecodeUs = scaleTimecodeToUs(value); - return; + break; case ID_BLOCK_DURATION: blockDurationUs = scaleTimecodeToUs(value); - return; + break; case ID_STEREO_MODE: int layout = (int) value; switch (layout) { @@ -677,9 +677,9 @@ public final class MatroskaExtractor implements Extractor { default: break; } - return; + break; default: - return; + break; } } @@ -687,12 +687,12 @@ public final class MatroskaExtractor implements Extractor { switch (id) { case ID_DURATION: durationTimecode = (long) value; - return; + break; case ID_SAMPLING_FREQUENCY: currentTrack.sampleRate = (int) value; - return; + break; default: - return; + break; } } @@ -703,15 +703,15 @@ public final class MatroskaExtractor implements Extractor { if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) { throw new ParserException("DocType " + value + " not supported"); } - return; + break; case ID_CODEC_ID: currentTrack.codecId = value; - return; + break; case ID_LANGUAGE: currentTrack.language = value; - return; + break; default: - return; + break; } } @@ -723,24 +723,24 @@ public final class MatroskaExtractor implements Extractor { input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize); seekEntryIdBytes.setPosition(0); seekEntryId = (int) seekEntryIdBytes.readUnsignedInt(); - return; + break; case ID_CODEC_PRIVATE: currentTrack.codecPrivate = new byte[contentSize]; input.readFully(currentTrack.codecPrivate, 0, contentSize); - return; + break; case ID_PROJECTION_PRIVATE: currentTrack.projectionData = new byte[contentSize]; input.readFully(currentTrack.projectionData, 0, contentSize); - return; + break; case ID_CONTENT_COMPRESSION_SETTINGS: // This extractor only supports header stripping, so the payload is the stripped bytes. currentTrack.sampleStrippedBytes = new byte[contentSize]; input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); - return; + break; case ID_CONTENT_ENCRYPTION_KEY_ID: currentTrack.encryptionKeyId = new byte[contentSize]; input.readFully(currentTrack.encryptionKeyId, 0, contentSize); - return; + break; case ID_SIMPLE_BLOCK: case ID_BLOCK: // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure @@ -873,7 +873,7 @@ public final class MatroskaExtractor implements Extractor { writeSampleData(input, track, blockLacingSampleSizes[0]); } - return; + break; default: throw new ParserException("Unexpected id: " + id); } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index c33d2abb89..5a2fc77c6c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -369,22 +369,22 @@ public final class Cea608Decoder extends CeaDecoder { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { captionStringBuilder.setLength(0); } - return; + break; case CTRL_ERASE_NON_DISPLAYED_MEMORY: captionStringBuilder.setLength(0); - return; + break; case CTRL_END_OF_CAPTION: captionString = getDisplayCaption(); captionStringBuilder.setLength(0); - return; + break; case CTRL_CARRIAGE_RETURN: maybeAppendNewline(); - return; + break; case CTRL_BACKSPACE: if (captionStringBuilder.length() > 0) { captionStringBuilder.setLength(captionStringBuilder.length() - 1); } - return; + break; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index 9059f3817f..c23b609704 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -113,8 +113,8 @@ public final class ParsingLoadable implements Loadable { inputStream.open(); result = parser.parse(dataSource.getUri(), inputStream); } finally { - inputStream.close(); bytesLoaded = inputStream.bytesRead(); + inputStream.close(); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index ad3569aca0..e3e887c6ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -354,10 +354,7 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - if (cachedContent == null) { - return false; - } - return cachedContent.isCached(position, length); + return cachedContent != null && cachedContent.isCached(position, length); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 10a473f177..c383c01453 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -98,7 +98,7 @@ public final class AtomicFile { baseName.delete(); } } - OutputStream str = null; + OutputStream str; try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e) { From 9ac7f64c8448551e5a5c3d69b40bb2d9e13c90f2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 22 Nov 2016 07:24:51 -0800 Subject: [PATCH 118/206] Fix search to end of stream in HLS ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139905590 --- .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 9e39b9adb5..6a90c8836f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -215,7 +215,7 @@ import java.util.Locale; int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; - if (targetPositionUs > mediaPlaylist.getEndTimeUs()) { + if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { From 6101450302b96d27a5464f262bffc349fab6bfa5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 22 Nov 2016 09:01:44 -0800 Subject: [PATCH 119/206] Merge initialization chunk into HlsMediaChunk This will allow creating the extractor in the HlsMediaChunk. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139915713 --- .../exoplayer2/source/hls/HlsChunkSource.java | 89 ++++++----------- .../source/hls/HlsInitializationChunk.java | 95 ------------------- .../exoplayer2/source/hls/HlsMediaChunk.java | 64 ++++++++++--- .../source/hls/HlsSampleStreamWrapper.java | 2 - 4 files changed, 81 insertions(+), 169 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/HlsInitializationChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 6a90c8836f..70174b8105 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -106,7 +106,6 @@ import java.util.Locale; private byte[] scratchSpace; private IOException fatalError; - private HlsInitializationChunk lastLoadedInitializationChunk; private Uri encryptionKeyUri; private byte[] encryptionKey; private String encryptionIvString; @@ -266,20 +265,17 @@ import java.util.Locale; clearEncryptionData(); } - // Compute start and end times, and the sequence number of the next chunk. + // Compute start time and sequence number of the next chunk. long startTimeUs = segment.startTimeUs; if (previous != null && !switchingVariant) { startTimeUs = previous.getAdjustedEndTimeUs(); } - long endTimeUs = startTimeUs + segment.durationUs; Format format = variants[newVariantIndex].format; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); // Set the extractor that will read the chunk. Extractor extractor; - boolean useInitializedExtractor = lastLoadedInitializationChunk != null - && lastLoadedInitializationChunk.format == format; boolean needNewExtractor = previous == null || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || format != previous.trackFormat; @@ -305,64 +301,56 @@ import java.util.Locale; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { isTimestampMaster = true; if (needNewExtractor) { - if (useInitializedExtractor) { - extractor = lastLoadedInitializationChunk.extractor; - } else { - timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); - } + timestampAdjuster = timestampAdjusterProvider.getAdjuster( + segment.discontinuitySequenceNumber, startTimeUs); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { + extractorNeedsInit = false; extractor = previous.extractor; } } else if (needNewExtractor) { // MPEG-2 TS segments, but we need a new extractor. isTimestampMaster = true; - if (useInitializedExtractor) { - extractor = lastLoadedInitializationChunk.extractor; - } else { - timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultTsPayloadReaderFactory.Flags - int esReaderFactoryFlags = 0; - String codecs = format.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can - // explicitly ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; - } + timestampAdjuster = timestampAdjusterProvider.getAdjuster( + segment.discontinuitySequenceNumber, startTimeUs); + // This flag ensures the change of pid between streams does not affect the sample queues. + @DefaultTsPayloadReaderFactory.Flags + int esReaderFactoryFlags = 0; + String codecs = format.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; } - extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); } + extractor = new TsExtractor(timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); } else { // MPEG-2 TS segments, and we need to continue using the same extractor. extractor = previous.extractor; extractorNeedsInit = false; } - // Initialize the extractor. - if (needNewExtractor && mediaPlaylist.initializationSegment != null - && !useInitializedExtractor) { - out.chunk = buildInitializationChunk(mediaPlaylist, extractor, format); - return; + DataSpec initDataSpec = null; + Segment initSegment = mediaPlaylist.initializationSegment; + if (initSegment != null) { + Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); + initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset, + initSegment.byterangeLength, null); } - lastLoadedInitializationChunk = null; // Configure the data source and spec for the chunk. DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, variants[newVariantIndex], + out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber, - isTimestampMaster, timestampAdjuster, extractor, extractorNeedsInit, switchingVariant, - encryptionKey, encryptionIv); + segment, chunkMediaSequence, isTimestampMaster, timestampAdjuster, extractor, + extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv); } /** @@ -376,9 +364,6 @@ import java.util.Locale; HlsMediaChunk mediaChunk = (HlsMediaChunk) chunk; playlistTracker.onChunkLoaded(mediaChunk.hlsUrl, mediaChunk.chunkIndex, mediaChunk.getAdjustedStartTimeUs()); - } - if (chunk instanceof HlsInitializationChunk) { - lastLoadedInitializationChunk = (HlsInitializationChunk) chunk; } else if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); @@ -419,18 +404,6 @@ import java.util.Locale; // Private methods. - private HlsInitializationChunk buildInitializationChunk(HlsMediaPlaylist mediaPlaylist, - Extractor extractor, Format format) { - Segment initSegment = mediaPlaylist.initializationSegment; - // The initialization segment is required before the actual media chunk. - Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); - DataSpec initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset, - initSegment.byterangeLength, null); - return new HlsInitializationChunk(dataSource, initDataSpec, - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractor, - format); - } - private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsInitializationChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsInitializationChunk.java deleted file mode 100644 index c571b2f9df..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsInitializationChunk.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.hls; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Util; - -import java.io.IOException; - -/** - * An HLS initialization chunk. Provides the extractor with information required for extracting the - * samples. - */ -/* package */ final class HlsInitializationChunk extends Chunk { - - public final Format format; - - public final Extractor extractor; - - private int bytesLoaded; - private volatile boolean loadCanceled; - - public HlsInitializationChunk(DataSource dataSource, DataSpec dataSpec, int trackSelectionReason, - Object trackSelectionData, Extractor extractor, Format format) { - super(dataSource, dataSpec, C.TRACK_TYPE_DEFAULT, null, trackSelectionReason, - trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); - this.extractor = extractor; - this.format = format; - } - - /** - * Sets the {@link HlsSampleStreamWrapper} that will receive the sample format information from - * the initialization chunk. - * - * @param output The output that will receive the format information. - */ - public void init(HlsSampleStreamWrapper output) { - extractor.init(output); - } - - @Override - public long bytesLoaded() { - return bytesLoaded; - } - - @Override - public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - - @Override - public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); - try { - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - try { - int result = Extractor.RESULT_CONTINUE; - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); - } - } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); - } - } finally { - dataSource.close(); - } - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 25435022c5..83343984b7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; @@ -54,13 +55,17 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final HlsUrl hlsUrl; + private final DataSource initDataSource; + private final DataSpec initDataSpec; private final boolean isEncrypted; private final boolean extractorNeedsInit; private final boolean shouldSpliceIn; private final boolean isMasterTimestampSource; private final TimestampAdjuster timestampAdjuster; + private int initSegmentBytesLoaded; private int bytesLoaded; + private boolean initLoadCompleted; private HlsSampleStreamWrapper extractorOutput; private long adjustedEndTimeUs; private volatile boolean loadCanceled; @@ -69,13 +74,12 @@ import java.util.concurrent.atomic.AtomicInteger; /** * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. + * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. - * @param startTimeUs The start time of the media contained by the chunk, in microseconds. - * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param segment The {@link Segment} for which this media chunk is created. * @param chunkIndex The media sequence number of the chunk. - * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param extractor The extractor to decode samples from the data. @@ -86,23 +90,26 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ - public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, HlsUrl hlsUrl, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, - TimestampAdjuster timestampAdjuster, Extractor extractor, boolean extractorNeedsInit, - boolean shouldSpliceIn, byte[] encryptionKey, byte[] encryptionIv) { + public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, + HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment, + int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, + Extractor extractor, boolean extractorNeedsInit, boolean shouldSpliceIn, byte[] encryptionKey, + byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + trackSelectionReason, trackSelectionData, segment.startTimeUs, + segment.startTimeUs + segment.durationUs, chunkIndex); + this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; this.extractor = extractor; this.extractorNeedsInit = extractorNeedsInit; this.shouldSpliceIn = shouldSpliceIn; // Note: this.dataSource and dataSource may be different. - adjustedEndTimeUs = endTimeUs; this.isEncrypted = this.dataSource instanceof Aes128DataSource; + initDataSource = dataSource; + discontinuitySequenceNumber = segment.discontinuitySequenceNumber; + adjustedEndTimeUs = endTimeUs; uid = UID_SOURCE.getAndIncrement(); } @@ -158,6 +165,37 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { + maybeLoadInitData(); + if (!loadCanceled) { + loadMedia(); + } + } + + // Private methods. + + private void maybeLoadInitData() throws IOException, InterruptedException { + if (!extractorNeedsInit || initLoadCompleted || initDataSpec == null) { + return; + } + DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); + try { + ExtractorInput input = new DefaultExtractorInput(initDataSource, + initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec)); + try { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractor.read(input, null); + } + } finally { + initSegmentBytesLoaded += (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + } + } finally { + Util.closeQuietly(dataSource); + } + initLoadCompleted = true; + } + + private void loadMedia() throws IOException, InterruptedException { // If we previously fed part of this chunk to the extractor, we need to skip it this time. For // encrypted content we need to skip the data by reading it through the source, so as to ensure // correct decryption of the remainder of the chunk. For clear content, we can request the @@ -193,13 +231,11 @@ import java.util.concurrent.atomic.AtomicInteger; bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } loadCompleted = true; } - // Private methods - /** * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in * order to decrypt the loaded data. Else returns the original. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 863b06ec38..cdd212df3a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -358,8 +358,6 @@ import java.util.LinkedList; HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; mediaChunk.init(this); mediaChunks.add(mediaChunk); - } else if (loadable instanceof HlsInitializationChunk) { - ((HlsInitializationChunk) loadable).init(this); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, From fa3d129b1468c58569e6c1c450a1486e9149ab84 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 22 Nov 2016 09:03:03 -0800 Subject: [PATCH 120/206] Fix some of the issues pointed by android lint tool ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=139915885 --- .../android/exoplayer2/util/AtomicFileTest.java | 2 +- .../exoplayer2/upstream/cache/CacheDataSink.java | 12 +++++++----- .../exoplayer2/upstream/cache/CacheEvictor.java | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index cbe4acbae5..7cdbb9a5b1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -50,7 +50,7 @@ public class AtomicFileTest extends InstrumentationTestCase { assertFalse(file.exists()); } - public void testWriteEndRead() throws Exception { + public void testWriteRead() throws Exception { OutputStream output = atomicFile.startWrite(); output.write(5); atomicFile.endWrite(output); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 22ec21374d..9984e7d152 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -138,6 +138,7 @@ public final class CacheDataSink implements DataSink { outputStreamBytesWritten = 0; } + @SuppressWarnings("ThrowFromFinallyBlock") private void closeCurrentOutputStream() throws IOException { if (outputStream == null) { return; @@ -150,13 +151,14 @@ public final class CacheDataSink implements DataSink { success = true; } finally { Util.closeQuietly(outputStream); - if (success) { - cache.commitFile(file); - } else { - file.delete(); - } outputStream = null; + File fileToCommit = file; file = null; + if (success) { + cache.commitFile(fileToCommit); + } else { + fileToCommit.delete(); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java index 627bb7e2f4..8944b45033 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java @@ -22,7 +22,7 @@ package com.google.android.exoplayer2.upstream.cache; public interface CacheEvictor extends Cache.Listener { /** - * Called when cache has beeen initialized. + * Called when cache has been initialized. */ void onCacheInitialized(); From b29ff0cf517cbb361c90edf208e855306999b7c7 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 23 Nov 2016 02:35:22 -0800 Subject: [PATCH 121/206] Make CacheDataSink use ReusableBufferedOutputStream ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140010664 --- .../upstream/cache/CacheDataSink.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 9984e7d152..d57f3ee140 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -20,8 +20,8 @@ import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ReusableBufferedOutputStream; import com.google.android.exoplayer2.util.Util; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -42,6 +42,7 @@ public final class CacheDataSink implements DataSink { private FileOutputStream underlyingFileOutputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; + private ReusableBufferedOutputStream bufferedOutputStream; /** * Thrown when IOException is encountered when writing data into sink. @@ -132,9 +133,17 @@ public final class CacheDataSink implements DataSink { file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); underlyingFileOutputStream = new FileOutputStream(file); - outputStream = bufferSize > 0 - ? new BufferedOutputStream(underlyingFileOutputStream, bufferSize) - : underlyingFileOutputStream; + if (bufferSize > 0) { + if (bufferedOutputStream == null) { + bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream, + bufferSize); + } else { + bufferedOutputStream.reset(underlyingFileOutputStream); + } + outputStream = bufferedOutputStream; + } else { + outputStream = underlyingFileOutputStream; + } outputStreamBytesWritten = 0; } From b3726cf7619b95e2c011b74bc99283aea956805f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 23 Nov 2016 03:00:16 -0800 Subject: [PATCH 122/206] Support DASH multi-segment fetches Note that multi-segment fetching is only possible in the case that segments in a representation are defined to have the same Uri and adjacent ranges (this is very rarely true for live streams, but is quite often true for on-demand). In the case that merging is requested but not possible, the implementation will request one at a time. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140012443 --- .../source/chunk/ContainerMediaChunk.java | 16 ++++- .../exoplayer2/source/chunk/MediaChunk.java | 2 +- .../source/dash/DefaultDashChunkSource.java | 62 ++++++++++++++----- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 130dddc5eb..a5af3cc42f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -32,8 +32,9 @@ import java.io.IOException; */ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMetadataOutput { - private final ChunkExtractorWrapper extractorWrapper; + private final int chunkCount; private final long sampleOffsetUs; + private final ChunkExtractorWrapper extractorWrapper; private final Format sampleFormat; private volatile int bytesLoaded; @@ -49,6 +50,9 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. + * @param chunkCount The number of chunks in the underlying media that are spanned by this + * instance. Normally equal to one, but may be larger if multiple chunks as defined by the + * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if @@ -56,15 +60,21 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, Format sampleFormat) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); - this.extractorWrapper = extractorWrapper; + this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; + this.extractorWrapper = extractorWrapper; this.sampleFormat = sampleFormat; } + @Override + public int getNextChunkIndex() { + return chunkIndex + chunkCount; + } + @Override public boolean isLoadCompleted() { return loadCompleted; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index d3e211c09f..3a02884fff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -53,7 +53,7 @@ public abstract class MediaChunk extends Chunk { /** * Returns the next chunk index. */ - public final int getNextChunkIndex() { + public int getNextChunkIndex() { return chunkIndex + 1; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 919a0231ea..0e3d127796 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -54,9 +55,15 @@ public class DefaultDashChunkSource implements DashChunkSource { public static final class Factory implements DashChunkSource.Factory { private final DataSource.Factory dataSourceFactory; + private final int maxSegmentsPerLoad; public Factory(DataSource.Factory dataSourceFactory) { + this(dataSourceFactory, 1); + } + + public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { this.dataSourceFactory = dataSourceFactory; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; } @Override @@ -65,7 +72,8 @@ public class DefaultDashChunkSource implements DashChunkSource { TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs); + adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, + maxSegmentsPerLoad); } } @@ -76,6 +84,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; + private final int maxSegmentsPerLoad; private DashManifest manifest; private int periodIndex; @@ -93,10 +102,13 @@ public class DefaultDashChunkSource implements DashChunkSource { * @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 maxSegmentsPerLoad The maximum number of segments to combine into a single request. + * Note that segments will only be combined if their {@link Uri}s are the same and if their + * data ranges are adjacent. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs) { + DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; @@ -104,6 +116,7 @@ public class DefaultDashChunkSource implements DashChunkSource { this.dataSource = dataSource; this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); List representations = getRepresentations(); @@ -219,9 +232,10 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } + int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), sampleFormat, segmentNum); + trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount); out.chunk = nextMediaChunk; } @@ -260,7 +274,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).chunkIndex >= lastAvailableSegmentNum) { + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { missingLastSegment = true; return true; } @@ -284,7 +298,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private Chunk newInitializationChunk(RepresentationHolder representationHolder, + private static Chunk newInitializationChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { RangedUri requestUri; @@ -305,24 +319,38 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } - private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, - Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int segmentNum) { + private static Chunk newMediaChunk(RepresentationHolder representationHolder, + DataSource dataSource, Format trackFormat, int trackSelectionReason, + Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; - long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); - long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); - RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); - DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(representation.baseUrl), - segmentUri.start, segmentUri.length, representation.getCacheKey()); - + long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); + RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); + String baseUrl = representation.baseUrl; if (representationHolder.extractorWrapper == null) { + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, segmentNum, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat); } else { + int segmentCount = 1; + for (int i = 1; i < maxSegmentCount; i++) { + RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i); + RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl); + if (mergedSegmentUri == null) { + // Unable to merge segment fetches because the URIs do not merge. + break; + } + segmentUri = mergedSegmentUri; + segmentCount++; + } + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, segmentNum, sampleOffsetUs, - representationHolder.extractorWrapper, sampleFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, + sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index f51280e0b9..aa197806e2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -218,7 +218,7 @@ public class DefaultSsChunkSource implements SsChunkSource { // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. long sampleOffsetUs = chunkStartTimeUs; return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, - trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, sampleOffsetUs, + trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, extractorWrapper, format); } From eb62d00ea40957931e02fe30929eaf938d87a955 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 23 Nov 2016 03:36:05 -0800 Subject: [PATCH 123/206] ExoPlayerImplInternal cleanup - Fix handling of the currently playing period being removed. This didn't do the right thing at all. - Relax rule on seekToPeriodPosition re-using an existing holder. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140014791 --- .../exoplayer2/ExoPlayerImplInternal.java | 121 ++++++++---------- 1 file changed, 53 insertions(+), 68 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 2b71163c6d..eb9890c545 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -166,11 +166,9 @@ import java.io.IOException; private SeekPosition pendingSeekPosition; private long rendererPositionUs; - private boolean isTimelineReady; - private boolean isTimelineEnded; - private MediaPeriodHolder playingPeriodHolder; - private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder loadingPeriodHolder; + private MediaPeriodHolder readingPeriodHolder; + private MediaPeriodHolder playingPeriodHolder; private Timeline timeline; @@ -451,7 +449,6 @@ import java.io.IOException; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = SystemClock.elapsedRealtime(); - updatePeriods(); if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. @@ -489,22 +486,27 @@ import java.io.IOException; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) - && isTimelineEnded) { + && playingPeriodHolder.isLast) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { - if ((enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) : isTimelineReady)) { + boolean isNewlyReady = enabledRenderers.length > 0 + ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) + : isTimelineReady(playingPeriodDurationUs); + if (isNewlyReady) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); } } - } else if (state == ExoPlayer.STATE_READY - && (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !isTimelineReady)) { - rebuffering = playWhenReady; - setState(ExoPlayer.STATE_BUFFERING); - stopRenderers(); + } else if (state == ExoPlayer.STATE_READY) { + boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded + : isTimelineReady(playingPeriodDurationUs); + if (!isStillReady) { + rebuffering = playWhenReady; + setState(ExoPlayer.STATE_BUFFERING); + stopRenderers(); + } } if (state == ExoPlayer.STATE_BUFFERING) { @@ -572,13 +574,6 @@ import java.io.IOException; rebuffering = false; setState(ExoPlayer.STATE_BUFFERING); - if (readingPeriodHolder != playingPeriodHolder && (periodIndex == playingPeriodHolder.index - || periodIndex == readingPeriodHolder.index)) { - // Clear the timeline because a renderer is reading ahead to the next period and the seek is - // to either the playing or reading period. - periodIndex = C.INDEX_UNSET; - } - MediaPeriodHolder newPlayingPeriodHolder = null; if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. @@ -598,8 +593,10 @@ import java.io.IOException; } } - // Disable all the renderers if the period is changing. - if (newPlayingPeriodHolder != playingPeriodHolder) { + // Disable all the renderers if the period being played is changing, or if the renderers are + // reading from a period other than the one being played. + if (playingPeriodHolder != newPlayingPeriodHolder + || playingPeriodHolder != readingPeriodHolder) { for (Renderer renderer : enabledRenderers) { renderer.disable(); } @@ -608,25 +605,24 @@ import java.io.IOException; rendererMediaClockSource = null; } - // Update loaded periods. + // Update the holders. if (newPlayingPeriodHolder != null) { newPlayingPeriodHolder.next = null; + loadingPeriodHolder = newPlayingPeriodHolder; + readingPeriodHolder = newPlayingPeriodHolder; setPlayingPeriodHolder(newPlayingPeriodHolder); - updateTimelineState(); - readingPeriodHolder = playingPeriodHolder; - loadingPeriodHolder = playingPeriodHolder; if (playingPeriodHolder.hasEnabledTracks) { periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - playingPeriodHolder = null; - readingPeriodHolder = null; loadingPeriodHolder = null; + readingPeriodHolder = null; + playingPeriodHolder = null; resetRendererPosition(periodPositionUs); } - updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); return periodPositionUs; } @@ -678,11 +674,9 @@ import java.io.IOException; mediaSource.releaseSource(); mediaSource = null; } - isTimelineReady = false; - isTimelineEnded = false; - playingPeriodHolder = null; - readingPeriodHolder = null; loadingPeriodHolder = null; + readingPeriodHolder = null; + playingPeriodHolder = null; timeline = null; setIsLoading(false); } @@ -739,8 +733,8 @@ import java.io.IOException; boolean recreateStreams = readingPeriodHolder != playingPeriodHolder; releasePeriodHoldersFrom(playingPeriodHolder.next); playingPeriodHolder.next = null; - readingPeriodHolder = playingPeriodHolder; loadingPeriodHolder = playingPeriodHolder; + readingPeriodHolder = playingPeriodHolder; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( @@ -802,6 +796,12 @@ import java.io.IOException; handler.sendEmptyMessage(MSG_DO_SOME_WORK); } + private boolean isTimelineReady(long playingPeriodDurationUs) { + return playingPeriodDurationUs == C.TIME_UNSET + || playbackInfo.positionUs < playingPeriodDurationUs + || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); + } + private boolean haveSufficientBuffer(boolean rebuffering) { if (loadingPeriodHolder == null) { return false; @@ -880,17 +880,23 @@ import java.io.IOException; stopInternal(); return; } - // Release all loaded periods. - releasePeriodHoldersFrom(periodHolder); - playingPeriodHolder = null; - readingPeriodHolder = null; - loadingPeriodHolder = null; - // Find the default initial position in the window and seek to it. + // We resolved a subsequent period. Seek to the default position in the corresponding window. Pair defaultPosition = getPeriodPosition( timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); newPeriodIndex = defaultPosition.first; - long newPlayingPositionUs = defaultPosition.second; - playbackInfo = new PlaybackInfo(newPeriodIndex, newPlayingPositionUs); + long newPositionUs = defaultPosition.second; + timeline.getPeriod(newPeriodIndex, period, true); + // Clear the index of each holder that doesn't contain the default position. If a holder + // contains the default position then update its index so it can be re-used when seeking. + Object newPeriodUid = period.uid; + periodHolder.index = C.INDEX_UNSET; + while (periodHolder.next != null) { + periodHolder = periodHolder.next; + periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET; + } + // Actually do the seek. + newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); + playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs); notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } @@ -900,13 +906,13 @@ import java.io.IOException; boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 && !timeline.getWindow(period.windowIndex, window).isDynamic; periodHolder.setIndex(periodIndex, isLastPeriod); + boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } // If there are subsequent holders, update the index for each of them. If we find a holder // that's inconsistent with the new timeline then take appropriate action. - boolean seenReadingPeriod = false; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; @@ -921,17 +927,11 @@ import java.io.IOException; } else { // The holder is inconsistent with the new timeline. if (!seenReadingPeriod) { - // Renderers may have read from a period that's been removed, so release all loaded - // periods and seek to the current position of the playing period index. + // Renderers may have read from a period that's been removed. Seek back to the current + // position of the playing period to make sure none of the removed period is played. periodIndex = playingPeriodHolder.index; - releasePeriodHoldersFrom(playingPeriodHolder); - playingPeriodHolder = null; - readingPeriodHolder = null; - loadingPeriodHolder = null; long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); - if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); - } + playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); } else { // Update the loading period to be the last period that's still valid, and release all // subsequent periods. @@ -1150,7 +1150,6 @@ import java.io.IOException; updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } - updateTimelineState(); if (readingPeriodHolder.isLast) { // The renderers have their final SampleStreams. @@ -1202,8 +1201,8 @@ import java.io.IOException; if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; + resetRendererPosition(readingPeriodHolder.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); - updateTimelineState(); } maybeContinueLoading(); } @@ -1242,12 +1241,7 @@ import java.io.IOException; } private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { - boolean isFirstPeriod = playingPeriodHolder == null; playingPeriodHolder = periodHolder; - if (isFirstPeriod) { - resetRendererPosition(playingPeriodHolder.startPositionUs); - } - int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -1274,15 +1268,6 @@ import java.io.IOException; enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } - private void updateTimelineState() { - long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) - .getDurationUs(); - isTimelineReady = playingPeriodDurationUs == C.TIME_UNSET - || playbackInfo.positionUs < playingPeriodDurationUs - || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); - isTimelineEnded = playingPeriodHolder.isLast; - } - private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) throws ExoPlaybackException { enabledRenderers = new Renderer[enabledRendererCount]; From a7dff14d3c14bd999bca7cb05fa8edf853b93362 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 23 Nov 2016 07:13:36 -0800 Subject: [PATCH 124/206] Keep TS packets with no continuity counter increase and no payload This behavior is defined in ISO-13818-1, section 2.4.3.3(continuity_counter). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140029161 --- .../android/exoplayer2/extractor/ts/TsExtractor.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 6808c14cf9..0403a970c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -216,9 +216,11 @@ public final class TsExtractor implements Extractor { int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { - // Duplicate packet found. - tsPacketBuffer.setPosition(endOfPacket); - return RESULT_CONTINUE; + if (payloadExists) { + // Duplicate packet found. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } } else if (continuityCounter != (previousCounter + 1) % 16) { discontinuityFound = true; } From 62bdb1b93a3f7344b669587d1108ca41e83ad418 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 23 Nov 2016 07:34:00 -0800 Subject: [PATCH 125/206] Fix failure when a seek is performed with no enabled tracks This issue affects ExtractorMediaSource only. We shouldn't start loading in the case that we're prepared and have no enabled tracks, since there's nothing that we need to load. This was causing an assertion failure in startLoading. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140030650 --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 3b18a06c75..13f33465d1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -231,7 +231,7 @@ import java.io.IOException; @Override public boolean continueLoading(long playbackPositionUs) { - if (loadingFinished) { + if (loadingFinished || (prepared && enabledTrackCount == 0)) { return false; } boolean continuedLoading = loadCondition.open(); From ba49f513802dc6412719d52e20f684dcd667ef95 Mon Sep 17 00:00:00 2001 From: cchiappini Date: Wed, 23 Nov 2016 10:00:33 -0800 Subject: [PATCH 126/206] Cronet README fix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140043801 --- extensions/cronet/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index be79ac7d3e..a570385a52 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -21,8 +21,9 @@ git clone https://github.com/google/ExoPlayer.git 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` directory -1. Download `cronet.jar`, `cronet_api.jar` and the `libs` directory -1. Copy the two jar files into the `libs` directory of this extension +1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`, + `cronet_impl_native_java.jar` and the `libs` directory +1. Copy the three jar files into the `libs` directory of this extension 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension From cbf5988803c658a911803f8f86d3900dc75bfffa Mon Sep 17 00:00:00 2001 From: mgersh Date: Wed, 23 Nov 2016 14:47:22 -0800 Subject: [PATCH 127/206] Switch to Cronet 56_0_2924_0 This is the second CL in the 3 CL process to switch Cronet versions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140074017 --- .../android/exoplayer2/ext/cronet/CronetDataSourceTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index c214503f0c..7efc542dd0 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -60,6 +60,7 @@ import org.chromium.net.CronetEngine; import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequestException; import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.UrlResponseInfoImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -138,7 +139,7 @@ public final class CronetDataSourceTest { private UrlResponseInfo createUrlResponseInfo(int statusCode) { ArrayList> responseHeaderList = new ArrayList<>(); responseHeaderList.addAll(testResponseHeader.entrySet()); - return new UrlResponseInfo( + return new UrlResponseInfoImpl( Collections.singletonList(TEST_URL), statusCode, null, // httpStatusText From 894ae1a310cc2e1fe5ba59eb56c7fbeb751036d5 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Nov 2016 09:08:10 -0800 Subject: [PATCH 128/206] Improve SimpleExoPlayer flexibility - Allow extension and overriding of renderer creation. Several developers have asked for this, so that they can use their own renderers (typically extensions to the core ones) without losing the ability to use SimpleExoPlayer. - Add option to not attempt extension renderer creation, for efficiency. - Align build variants for internal and external demo apps. This is slightly unfortunate, but convergence seems necessary for useExtensionRenderers. - Fix DASH playback tests to use the debug video renderer. Issue #2102 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140140915 --- demo/build.gradle | 19 +- .../exoplayer2/demo/DemoApplication.java | 10 +- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../android/exoplayer2/ExoPlayerFactory.java | 26 +-- .../android/exoplayer2/SimpleExoPlayer.java | 199 ++++++++++++++---- .../playbacktests/gts/DashTest.java | 17 +- .../util/DebugMediaCodecVideoRenderer.java | 111 ---------- .../util/DebugSimpleExoPlayer.java | 142 +++++++++++++ .../playbacktests/util/ExoHostedTest.java | 3 +- 9 files changed, 358 insertions(+), 176 deletions(-) delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java diff --git a/demo/build.gradle b/demo/build.gradle index bfbcd1aa4c..2c01cbbe73 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -37,16 +37,23 @@ android { abortOnError false } + flavorDimensions "extensions" + productFlavors { - noExtensions - withExtensions + noExtns { + dimension "extensions" + } + extns { + dimension "extensions" + } } + } dependencies { compile project(':library') - withExtensionsCompile project(path: ':extension-ffmpeg') - withExtensionsCompile project(path: ':extension-flac') - withExtensionsCompile project(path: ':extension-opus') - withExtensionsCompile project(path: ':extension-vp9') + extnsCompile project(path: ':extension-ffmpeg') + extnsCompile project(path: ':extension-flac') + extnsCompile project(path: ':extension-opus') + extnsCompile project(path: ':extension-vp9') } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 92dc08597f..a7e67d169a 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -36,13 +36,19 @@ public class DemoApplication extends Application { userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); } - DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { + public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { return new DefaultDataSourceFactory(this, bandwidthMeter, buildHttpDataSourceFactory(bandwidthMeter)); } - HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { + public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); } + public boolean useExtensionRenderers() { + // We should return BuildConfig.FLAVOR_extensions.equals("extns") here, but this is currently + // incompatible with a Google internal build system. + return true; + } + } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 5351890d6f..7589d54810 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -251,12 +251,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode = + ((DemoApplication) getApplication()).useExtensionRenderers() + ? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER + : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) + : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), - drmSessionManager, preferExtensionDecoders); + drmSessionManager, extensionRendererMode); player.addListener(this); eventLogger = new EventLogger(trackSelector); diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index e43a9c0357..43de6fe751 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -49,7 +49,7 @@ public final class ExoPlayerFactory { /** * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * {@link Looper}. Available extension renderers are not used. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -59,7 +59,8 @@ public final class ExoPlayerFactory { */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager) { - return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false); + return newSimpleInstance(context, trackSelector, loadControl, + drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF); } /** @@ -71,15 +72,15 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in - * available extensions over those defined in the core library. Note that extensions must be - * included in the application build for setting this flag to have any effect. + * @param extensionRendererMode The extension renderer mode, which determines if and how available + * extension renderers are used. Note that extensions must be included in the application + * build for them to be considered available. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - boolean preferExtensionDecoders) { + @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) { return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, - preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } /** @@ -91,17 +92,18 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in - * available extensions over those defined in the core library. Note that extensions must be - * included in the application build for setting this flag to have any effect. + * @param extensionRendererMode The extension renderer mode, which determines if and how available + * extension renderers are used. Note that extensions must be included in the application + * build for them to be considered available. * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * seamlessly join an ongoing playback. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { + @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode, + long allowedVideoJoiningTimeMs) { return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, - preferExtensionDecoders, allowedVideoJoiningTimeMs); + extensionRendererMode, allowedVideoJoiningTimeMs); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ca193c576b..36753309e2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -21,6 +21,7 @@ import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; +import android.support.annotation.IntDef; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; @@ -45,6 +46,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; @@ -54,7 +57,7 @@ import java.util.List; * be obtained from {@link ExoPlayerFactory}. */ @TargetApi(16) -public final class SimpleExoPlayer implements ExoPlayer { +public class SimpleExoPlayer implements ExoPlayer { /** * A listener for video rendering information from a {@link SimpleExoPlayer}. @@ -88,8 +91,33 @@ public final class SimpleExoPlayer implements ExoPlayer { } + /** + * Modes for using extension renderers. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) + public @interface ExtensionRendererMode {} + /** + * Do not allow use of extension renderers. + */ + public static final int EXTENSION_RENDERER_MODE_OFF = 0; + /** + * Allow use of extension renderers. Extension renderers are indexed after core renderers of the + * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore + * prefer to use a core renderer to an extension renderer in the case that both are able to play + * a given track. + */ + public static final int EXTENSION_RENDERER_MODE_ON = 1; + /** + * Allow use of extension renderers. Extension renderers are indexed before core renderers of the + * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore + * prefer to use an extension renderer to a core renderer in the case that both are able to play + * a given track. + */ + public static final int EXTENSION_RENDERER_MODE_PREFER = 2; + private static final String TAG = "SimpleExoPlayer"; - private static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final ExoPlayer player; private final Renderer[] renderers; @@ -120,21 +148,16 @@ public final class SimpleExoPlayer implements ExoPlayer { private float audioVolume; private PlaybackParamsHolder playbackParamsHolder; - /* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager, - boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { + protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { mainHandler = new Handler(); componentListener = new ComponentListener(); // Build the renderers. ArrayList renderersList = new ArrayList<>(); - if (preferExtensionDecoders) { - buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs); - buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs); - } else { - buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs); - buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs); - } + buildRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, + allowedVideoJoiningTimeMs, renderersList); renderers = renderersList.toArray(new Renderer[renderersList.size()]); // Obtain counts of video and audio renderers. @@ -593,54 +616,99 @@ public final class SimpleExoPlayer implements ExoPlayer { return player.getCurrentManifest(); } - // Internal methods. + // Renderer building. - private void buildRenderers(Context context, - DrmSessionManager drmSessionManager, ArrayList renderersList, - long allowedVideoJoiningTimeMs) { - MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer(context, - MediaCodecSelector.DEFAULT, allowedVideoJoiningTimeMs, drmSessionManager, false, - mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - renderersList.add(videoRenderer); - - Renderer audioRenderer = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, - drmSessionManager, true, mainHandler, componentListener, - AudioCapabilities.getCapabilities(context)); - renderersList.add(audioRenderer); - - Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper()); - renderersList.add(textRenderer); - - MetadataRenderer metadataRenderer = new MetadataRenderer(componentListener, - mainHandler.getLooper(), new Id3Decoder()); - renderersList.add(metadataRenderer); + private void buildRenderers(Context context, Handler mainHandler, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs, + ArrayList out) { + buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, + componentListener, allowedVideoJoiningTimeMs, out); + buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, + componentListener, out); + buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); + buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); + buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); } - private void buildExtensionRenderers(ArrayList renderersList, - long allowedVideoJoiningTimeMs) { - // Load extension renderers using reflection so that demo app doesn't depend on them. - // Class.forName() appears for each renderer so that automated tools like proguard - // can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname). + /** + * Builds video renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param mainHandler A handler associated with the main thread's looper. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param extensionRendererMode The extension renderer mode. + * @param eventListener An event listener. + * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers + * can attempt to seamlessly join an ongoing playback. + * @param out An array to which the built renderers should be appended. + */ + protected void buildVideoRenderers(Context context, Handler mainHandler, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, ArrayList out) { + out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, + allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.size(); + if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { + extensionRendererIndex--; + } + try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); Constructor constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, VideoRendererEventListener.class, int.class); - renderersList.add((Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs, - mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs, + mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibvpxVideoRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { throw new RuntimeException(e); } + } + + /** + * Builds audio renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param mainHandler A handler associated with the main thread's looper. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param extensionRendererMode The extension renderer mode. + * @param eventListener An event listener. + * @param out An array to which the built renderers should be appended. + */ + protected void buildAudioRenderers(Context context, Handler mainHandler, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, + ArrayList out) { + out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, + mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.size(); + if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { + extensionRendererIndex--; + } try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class); - renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener)); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. @@ -653,7 +721,8 @@ public final class SimpleExoPlayer implements ExoPlayer { Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class); - renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener)); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. @@ -666,7 +735,8 @@ public final class SimpleExoPlayer implements ExoPlayer { Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class); - renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener)); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. @@ -675,6 +745,51 @@ public final class SimpleExoPlayer implements ExoPlayer { } } + /** + * Builds text renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param mainHandler A handler associated with the main thread's looper. + * @param extensionRendererMode The extension renderer mode. + * @param output An output for the renderers. + * @param out An array to which the built renderers should be appended. + */ + protected void buildTextRenderers(Context context, Handler mainHandler, + @ExtensionRendererMode int extensionRendererMode, TextRenderer.Output output, + ArrayList out) { + out.add(new TextRenderer(output, mainHandler.getLooper())); + } + + /** + * Builds metadata renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param mainHandler A handler associated with the main thread's looper. + * @param extensionRendererMode The extension renderer mode. + * @param output An output for the renderers. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMetadataRenderers(Context context, Handler mainHandler, + @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output, + ArrayList out) { + out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder())); + } + + /** + * Builds any miscellaneous renderers used by the player. + * + * @param context The {@link Context} associated with the player. + * @param mainHandler A handler associated with the main thread's looper. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMiscellaneousRenderers(Context context, Handler mainHandler, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + // Do nothing. + } + + // Internal methods. + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 8aee627993..da9f1591e7 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -21,11 +21,15 @@ import android.media.UnsupportedSchemeException; import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; +import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; @@ -34,6 +38,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; import com.google.android.exoplayer2.playbacktests.util.HostActivity; @@ -727,7 +732,17 @@ public final class DashTest extends ActivityInstrumentationTestCase2 drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, TransferListener mediaTransferListener) { DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java deleted file mode 100644 index e38dea948a..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugMediaCodecVideoRenderer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2016 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.playbacktests.util; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Handler; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; -import com.google.android.exoplayer2.video.VideoRendererEventListener; - -/** - * Decodes and renders video using {@link MediaCodecVideoRenderer}. Provides buffer timestamp - * assertions. - */ -@TargetApi(16) -public class DebugMediaCodecVideoRenderer extends MediaCodecVideoRenderer { - - private static final int ARRAY_SIZE = 1000; - - private final long[] timestampsList = new long[ARRAY_SIZE]; - - private int startIndex; - private int queueSize; - private int bufferCount; - - public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, - int maxDroppedFrameCountToNotify) { - super(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, - eventListener, maxDroppedFrameCountToNotify); - startIndex = 0; - queueSize = 0; - } - - @Override - protected void releaseCodec() { - super.releaseCodec(); - clearTimestamps(); - } - - @Override - protected void flushCodec() throws ExoPlaybackException { - super.flushCodec(); - clearTimestamps(); - } - - @Override - protected void onQueueInputBuffer(DecoderInputBuffer buffer) { - insertTimestamp(buffer.timeUs); - maybeShiftTimestampsList(); - } - - @Override - protected void onProcessedOutputBuffer(long presentationTimeUs) { - bufferCount++; - long expectedTimestampUs = dequeueTimestamp(); - if (expectedTimestampUs != presentationTimeUs) { - throw new IllegalStateException("Expected to dequeue video buffer with presentation " - + "timestamp: " + expectedTimestampUs + ". Instead got: " + presentationTimeUs - + " (Processed buffers since last flush: " + bufferCount + ")."); - } - } - - private void clearTimestamps() { - startIndex = 0; - queueSize = 0; - bufferCount = 0; - } - - private void insertTimestamp(long presentationTimeUs) { - for (int i = startIndex + queueSize - 1; i >= startIndex; i--) { - if (presentationTimeUs >= timestampsList[i]) { - timestampsList[i + 1] = presentationTimeUs; - queueSize++; - return; - } - timestampsList[i + 1] = timestampsList[i]; - } - timestampsList[startIndex] = presentationTimeUs; - queueSize++; - } - - private void maybeShiftTimestampsList() { - if (startIndex + queueSize == ARRAY_SIZE) { - System.arraycopy(timestampsList, startIndex, timestampsList, 0, queueSize); - startIndex = 0; - } - } - - private long dequeueTimestamp() { - startIndex++; - queueSize--; - return timestampsList[startIndex - 1]; - } -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java new file mode 100644 index 0000000000..6620c0dcf1 --- /dev/null +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 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.playbacktests.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Handler; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; + +/** + * A debug extension of {@link SimpleExoPlayer}. Provides video buffer timestamp assertions. + */ +@TargetApi(16) +public class DebugSimpleExoPlayer extends SimpleExoPlayer { + + public DebugSimpleExoPlayer(Context context, TrackSelector trackSelector, + LoadControl loadControl, DrmSessionManager drmSessionManager) { + super(context, trackSelector, loadControl, drmSessionManager, + SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF, 0); + } + + @Override + protected void buildVideoRenderers(Context context, Handler mainHandler, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, ArrayList out) { + out.add(new DebugMediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, + allowedVideoJoiningTimeMs, mainHandler, eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + } + + /** + * Decodes and renders video using {@link MediaCodecVideoRenderer}. Provides buffer timestamp + * assertions. + */ + private static class DebugMediaCodecVideoRenderer extends MediaCodecVideoRenderer { + + private static final int ARRAY_SIZE = 1000; + + private final long[] timestampsList = new long[ARRAY_SIZE]; + + private int startIndex; + private int queueSize; + private int bufferCount; + + public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, + int maxDroppedFrameCountToNotify) { + super(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, + eventListener, maxDroppedFrameCountToNotify); + startIndex = 0; + queueSize = 0; + } + + @Override + protected void releaseCodec() { + super.releaseCodec(); + clearTimestamps(); + } + + @Override + protected void flushCodec() throws ExoPlaybackException { + super.flushCodec(); + clearTimestamps(); + } + + @Override + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + insertTimestamp(buffer.timeUs); + maybeShiftTimestampsList(); + } + + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + bufferCount++; + long expectedTimestampUs = dequeueTimestamp(); + if (expectedTimestampUs != presentationTimeUs) { + throw new IllegalStateException("Expected to dequeue video buffer with presentation " + + "timestamp: " + expectedTimestampUs + ". Instead got: " + presentationTimeUs + + " (Processed buffers since last flush: " + bufferCount + ")."); + } + } + + private void clearTimestamps() { + startIndex = 0; + queueSize = 0; + bufferCount = 0; + } + + private void insertTimestamp(long presentationTimeUs) { + for (int i = startIndex + queueSize - 1; i >= startIndex; i--) { + if (presentationTimeUs >= timestampsList[i]) { + timestampsList[i + 1] = presentationTimeUs; + queueSize++; + return; + } + timestampsList[i + 1] = timestampsList[i]; + } + timestampsList[startIndex] = presentationTimeUs; + queueSize++; + } + + private void maybeShiftTimestampsList() { + if (startIndex + queueSize == ARRAY_SIZE) { + System.arraycopy(timestampsList, startIndex, timestampsList, 0, queueSize); + startIndex = 0; + } + } + + private long dequeueTimestamp() { + startIndex++; + queueSize--; + return timestampsList[startIndex - 1]; + } + + } + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 78f71e6415..dfecdd236a 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -320,7 +320,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(host, trackSelector, - new DefaultLoadControl(), drmSessionManager, false, 0); + new DefaultLoadControl(), drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF, + 0); player.setVideoSurface(surface); return player; } From 97a23ce5726757d82eba0b64cc2cb5d0cfd011f4 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 25 Nov 2016 06:13:35 -0800 Subject: [PATCH 129/206] Fix main thread playlist refreshes in HlsPlaylistTracker The refresh handler in HlsPlaylistTracker was being instantiated in the same thread as the MediaSource (i.e. Main thread). Issue:#2108 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140197553 --- .../android/exoplayer2/source/hls/HlsMediaSource.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 6fd82df316..f418bbded3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -42,11 +42,12 @@ public final class HlsMediaSource implements MediaSource, */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final HlsPlaylistTracker playlistTracker; + private final Uri manifestUri; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; + private HlsPlaylistTracker playlistTracker; private MediaSource.Listener sourceListener; public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, @@ -58,15 +59,17 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this); } @Override public void prepareSource(MediaSource.Listener listener) { + Assertions.checkState(playlistTracker == null); + playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, + minLoadableRetryCount, this); sourceListener = listener; playlistTracker.start(); } @@ -91,6 +94,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void releaseSource() { playlistTracker.release(); + playlistTracker = null; sourceListener = null; } From ce9ec79e5977e76512162dae355a596727e02b45 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Nov 2016 07:21:51 -0800 Subject: [PATCH 130/206] Make Timeline always non-null from ExoPlayer Note that we still have null timelines in ExoPlayerImplInternal. This is deliberate; and is likely necessary to distinguish between the no-timeline-yet and timeline-is-empty cases (we want to try and process a pending seek for the latter, but not the former). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140200980 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 4 +- .../android/exoplayer2/ExoPlayerImpl.java | 17 ++++---- .../google/android/exoplayer2/Timeline.java | 40 +++++++++++++++++++ .../exoplayer2/ui/PlaybackControlView.java | 8 ++-- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 7589d54810..413157cbd0 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -360,7 +360,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay playerWindow = player.getCurrentWindowIndex(); playerPosition = C.TIME_UNSET; Timeline timeline = player.getCurrentTimeline(); - if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) { + if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) { playerPosition = player.getCurrentPosition(); } player.release(); @@ -417,7 +417,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - isTimelineStatic = timeline != null && timeline.getWindowCount() > 0 + isTimelineStatic = !timeline.isEmpty() && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic; } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 83e4fd7e30..c84e6f9985 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -403,12 +403,12 @@ public interface ExoPlayer { /** * Returns the current manifest. The type depends on the {@link MediaSource} passed to - * {@link #prepare}. + * {@link #prepare}. May be null. */ Object getCurrentManifest(); /** - * Returns the current {@link Timeline}, or {@code null} if there is no timeline. + * Returns the current {@link Timeline}. Never null, but may be empty. */ Timeline getCurrentTimeline(); diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 5bf1b599e2..af4416e4a1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -82,6 +82,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playbackState = STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); + timeline = Timeline.EMPTY; window = new Timeline.Window(); period = new Timeline.Period(); trackGroups = TrackGroupArray.EMPTY; @@ -120,8 +121,8 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { if (resetState) { - if (timeline != null || manifest != null) { - timeline = null; + if (!timeline.isEmpty() || manifest != null) { + timeline = Timeline.EMPTY; manifest = null; for (EventListener listener : listeners) { listener.onTimelineChanged(null, null); @@ -178,7 +179,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void seekTo(int windowIndex, long positionMs) { - if (windowIndex < 0 || (timeline != null && windowIndex >= timeline.getWindowCount())) { + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IndexOutOfBoundsException(); } pendingSeekAcks++; @@ -223,7 +224,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getCurrentWindowIndex() { - if (timeline == null || pendingSeekAcks > 0) { + if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowIndex; } else { return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex; @@ -232,7 +233,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public long getDuration() { - if (timeline == null) { + if (timeline.isEmpty()) { return C.TIME_UNSET; } return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); @@ -240,7 +241,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public long getCurrentPosition() { - if (timeline == null || pendingSeekAcks > 0) { + if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { timeline.getPeriod(playbackInfo.periodIndex, period); @@ -251,7 +252,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public long getBufferedPosition() { // TODO - Implement this properly. - if (timeline == null || pendingSeekAcks > 0) { + if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { timeline.getPeriod(playbackInfo.periodIndex, period); @@ -261,7 +262,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getBufferedPercentage() { - if (timeline == null) { + if (timeline.isEmpty()) { return 0; } long bufferedPosition = getBufferedPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/Timeline.java index 1b0d03676b..32af48bd59 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -91,6 +91,46 @@ package com.google.android.exoplayer2; */ public abstract class Timeline { + /** + * An empty timeline. + */ + public static final Timeline EMPTY = new Timeline() { + + @Override + public int getWindowCount() { + return 0; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getPeriodCount() { + return 0; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + + }; + + /** + * Returns whether the timeline is empty. + */ + public final boolean isEmpty() { + return getWindowCount() == 0; + } + /** * Returns the number of windows in the timeline. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 29772dcc89..83f1615310 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -422,11 +422,11 @@ public class PlaybackControlView extends FrameLayout { return; } Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; - boolean haveTimeline = currentTimeline != null; + boolean haveNonEmptyTimeline = currentTimeline != null && !currentTimeline.isEmpty(); boolean isSeekable = false; boolean enablePrevious = false; boolean enableNext = false; - if (haveTimeline) { + if (haveNonEmptyTimeline) { int currentWindowIndex = player.getCurrentWindowIndex(); currentTimeline.getWindow(currentWindowIndex, currentWindow); isSeekable = currentWindow.isSeekable; @@ -525,7 +525,7 @@ public class PlaybackControlView extends FrameLayout { private void previous() { Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline == null) { + if (currentTimeline.isEmpty()) { return; } int currentWindowIndex = player.getCurrentWindowIndex(); @@ -540,7 +540,7 @@ public class PlaybackControlView extends FrameLayout { private void next() { Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline == null) { + if (currentTimeline.isEmpty()) { return; } int currentWindowIndex = player.getCurrentWindowIndex(); From 9c612f94c5f8aee93b36365dc9d495eeb38a3502 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 25 Nov 2016 07:30:23 -0800 Subject: [PATCH 131/206] Chose timestamp adjustment master based on track selection Select the timestamp master depending on track availability. If a variant is being loaded, then that is the timestmap master. Otherwise, if an audio track is being loaded, then the responsible chunk source is the timestmap master. If no variant or audio rendition is enabled, then a subtitle chunk source is selected as timestamp master. This CL will become specially relevant once ID3 PRIV timestamps are used for audio renditions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140201385 --- .../exoplayer2/source/hls/HlsChunkSource.java | 14 +++- .../exoplayer2/source/hls/HlsMediaPeriod.java | 73 +++++++++++-------- .../source/hls/HlsSampleStreamWrapper.java | 4 + 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 70174b8105..80c378a666 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -103,6 +103,7 @@ import java.util.Locale; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; + private boolean isTimestampMaster; private byte[] scratchSpace; private IOException fatalError; @@ -176,6 +177,16 @@ import java.util.Locale; fatalError = null; } + /** + * Sets whether this chunk source is responsible for initializing timestamp adjusters. + * + * @param isTimestampMaster True if this chunk source is responsible for initializing timestamp + * adjusters. + */ + public void setIsTimestampMaster(boolean isTimestampMaster) { + this.isTimestampMaster = isTimestampMaster; + } + /** * Returns the next chunk to load. *

    @@ -280,7 +291,6 @@ import java.util.Locale; || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || format != previous.trackFormat; boolean extractorNeedsInit = true; - boolean isTimestampMaster = false; TimestampAdjuster timestampAdjuster = null; String lastPathSegment = chunkUri.getLastPathSegment(); if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { @@ -299,7 +309,6 @@ import java.util.Locale; startTimeUs); extractor = new WebvttExtractor(format.language, timestampAdjuster); } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { - isTimestampMaster = true; if (needNewExtractor) { timestampAdjuster = timestampAdjusterProvider.getAdjuster( segment.discontinuitySequenceNumber, startTimeUs); @@ -310,7 +319,6 @@ import java.util.Locale; } } else if (needNewExtractor) { // MPEG-2 TS segments, but we need a new extractor. - isTimestampMaster = true; timestampAdjuster = timestampAdjusterProvider.getAdjuster( segment.discontinuitySequenceNumber, startTimeUs); // This flag ensures the change of pid between streams does not affect the sample queues. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 3951b30a78..be07b3410e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -166,6 +167,18 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Update the local state. enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); + + // The first enabled sample stream wrapper is responsible for intializing the timestamp + // adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are. + // If only subtitles are present, then text renditions are used for timestamp adjustment + // initialization. + if (enabledSampleStreamWrappers.length > 0) { + enabledSampleStreamWrappers[0].setIsTimestampMaster(true); + for (int i = 1; i < enabledSampleStreamWrappers.length; i++) { + enabledSampleStreamWrappers[i].setIsTimestampMaster(false); + } + } + sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); if (seenFirstTrackSelection && selectedNewTracks) { seekToUs(positionUs); @@ -241,7 +254,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl url) { + public void onPlaylistRefreshRequired(HlsUrl url) { playlistTracker.refreshPlaylist(url, this); } @@ -269,7 +282,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public void onPlaylistLoadError(HlsMasterPlaylist.HlsUrl url, IOException error) { + public void onPlaylistLoadError(HlsUrl url, IOException error) { for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { sampleStreamWrapper.onPlaylistLoadError(url, error); } @@ -281,11 +294,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private void buildAndPrepareSampleStreamWrappers() { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); // Build the default stream wrapper. - List selectedVariants = new ArrayList<>(masterPlaylist.variants); - ArrayList definiteVideoVariants = new ArrayList<>(); - ArrayList definiteAudioOnlyVariants = new ArrayList<>(); + List selectedVariants = new ArrayList<>(masterPlaylist.variants); + ArrayList definiteVideoVariants = new ArrayList<>(); + ArrayList definiteAudioOnlyVariants = new ArrayList<>(); for (int i = 0; i < selectedVariants.size(); i++) { - HlsMasterPlaylist.HlsUrl variant = selectedVariants.get(i); + HlsUrl variant = selectedVariants.get(i); if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { definiteVideoVariants.add(variant); } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { @@ -304,41 +317,44 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } else { // Leave the enabled variants unchanged. They're likely either all video or all audio. } - List audioVariants = masterPlaylist.audios; - List subtitleVariants = masterPlaylist.subtitles; - sampleStreamWrappers = new HlsSampleStreamWrapper[(selectedVariants.isEmpty() ? 0 : 1) - + audioVariants.size() + subtitleVariants.size()]; + List audioRenditions = masterPlaylist.audios; + List subtitleRenditions = masterPlaylist.subtitles; + sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size() + + subtitleRenditions.size()]; int currentWrapperIndex = 0; pendingPrepareCount = sampleStreamWrappers.length; - if (!selectedVariants.isEmpty()) { - HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; - selectedVariants.toArray(variants); - HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - sampleStreamWrapper.continuePreparing(); - } + + Assertions.checkArgument(!selectedVariants.isEmpty()); + HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; + selectedVariants.toArray(variants); + HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + sampleStreamWrapper.setIsTimestampMaster(true); + sampleStreamWrapper.continuePreparing(); + + // TODO: Build video stream wrappers here. // Build audio stream wrappers. - for (int i = 0; i < audioVariants.size(); i++) { - HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); + for (int i = 0; i < audioRenditions.size(); i++) { + sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, + new HlsUrl[] {audioRenditions.get(i)}, null, null); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } // Build subtitle stream wrappers. - for (int i = 0; i < subtitleVariants.size(); i++) { - HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i); - HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, - new HlsMasterPlaylist.HlsUrl[] {url}, null, null); + for (int i = 0; i < subtitleRenditions.size(); i++) { + HlsUrl url = subtitleRenditions.get(i); + sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, + null); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, - HlsMasterPlaylist.HlsUrl[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { + private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, + Format muxedAudioFormat, Format muxedCaptionFormat) { DataSource dataSource = dataSourceFactory.createDataSource(); HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, timestampAdjusterProvider); @@ -347,8 +363,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper eventDispatcher); } - private static boolean variantHasExplicitCodecWithPrefix(HlsMasterPlaylist.HlsUrl variant, - String prefix) { + private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { String codecs = variant.format.codecs; if (TextUtils.isEmpty(codecs)) { return false; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index cdd212df3a..bc44c84e39 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -275,6 +275,10 @@ import java.util.LinkedList; return largestQueuedTimestampUs; } + public void setIsTimestampMaster(boolean isTimestampMaster) { + chunkSource.setIsTimestampMaster(isTimestampMaster); + } + public void onPlaylistLoadError(HlsUrl url, IOException error) { chunkSource.onPlaylistLoadError(url, error); } From 76c58a34d32d06832f625115fae7df432b30b811 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 25 Nov 2016 08:02:45 -0800 Subject: [PATCH 132/206] Clarify createPeriod javadoc. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140202942 --- .../com/google/android/exoplayer2/source/MediaSource.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 709a92cbf5..29c7dd63a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -52,7 +52,9 @@ public interface MediaSource { void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a {@link MediaPeriod} corresponding to the period at the specified index. + * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}. + * This method may be called multiple times with the same index without an intervening call to + * {@link #releasePeriod(MediaPeriod)}. * * @param index The index of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. From ee9b7be2fa68d15ec505be1fd69740144fd61b46 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Nov 2016 08:18:15 -0800 Subject: [PATCH 133/206] Fix issue with seeking before timeline available ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140204054 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index eb9890c545..0e0c30ffc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -981,9 +981,9 @@ import java.io.IOException; */ private Pair resolveSeekPosition(SeekPosition seekPosition) { Timeline seekTimeline = seekPosition.timeline; - if (seekTimeline == null) { - // The application performed a blind seek without a timeline (most likely based on knowledge - // of what the timeline will be). Use the internal timeline. + if (seekTimeline.isEmpty()) { + // The application performed a blind seek without a non-empty timeline (most likely based on + // knowledge of what the future timeline will be). Use the internal timeline. seekTimeline = timeline; Assertions.checkIndex(seekPosition.windowIndex, 0, timeline.getWindowCount()); } From 2e3ffe1e94a16d6ca1e5c09f818969cbcd531abe Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Nov 2016 10:17:55 -0800 Subject: [PATCH 134/206] Assume support for vertical video if rotated resolution supported Issue: #2034 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140209306 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index a32b4a181f..835e2f69b4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -153,8 +153,14 @@ public final class MediaCodecInfo { return false; } if (!videoCapabilities.isSizeSupported(width, height)) { - logNoSupport("size.support, " + width + "x" + height); - return false; + // Capabilities are known to be inaccurately reported for vertical resolutions on some devices + // (b/31387661). If the video is vertical and the capabilities indicate support if the width + // and height are swapped, we assume that the vertical resolution is also supported. + if (width >= height || !videoCapabilities.isSizeSupported(height, width)) { + logNoSupport("size.support, " + width + "x" + height); + return false; + } + logAssumedSupport("size.rotated, " + width + "x" + height); } return true; } @@ -181,8 +187,14 @@ public final class MediaCodecInfo { return false; } if (!videoCapabilities.areSizeAndRateSupported(width, height, frameRate)) { - logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); - return false; + // Capabilities are known to be inaccurately reported for vertical resolutions on some devices + // (b/31387661). If the video is vertical and the capabilities indicate support if the width + // and height are swapped, we assume that the vertical resolution is also supported. + if (width >= height || !videoCapabilities.areSizeAndRateSupported(height, width, frameRate)) { + logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); + return false; + } + logAssumedSupport("sizeAndRate.rotated, " + width + "x" + height + "x" + frameRate); } return true; } @@ -240,7 +252,12 @@ public final class MediaCodecInfo { } private void logNoSupport(String message) { - Log.d(TAG, "FalseCheck [" + message + "] [" + name + ", " + mimeType + "] [" + Log.d(TAG, "NoSupport [" + message + "] [" + name + ", " + mimeType + "] [" + + Util.DEVICE_DEBUG_INFO + "]"); + } + + private void logAssumedSupport(String message) { + Log.d(TAG, "AssumedSupport [" + message + "] [" + name + ", " + mimeType + "] [" + Util.DEVICE_DEBUG_INFO + "]"); } From c3c176d93c03fe9578161d35f4552d453a3a314e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 25 Nov 2016 10:26:05 -0800 Subject: [PATCH 135/206] Move HLS extractor construction to HlsMediaChunk This allows ID3 PRIV timestamp extraction and Extractor Sniffing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140209568 --- .../exoplayer2/source/hls/HlsChunkSource.java | 85 +-------------- .../exoplayer2/source/hls/HlsMediaChunk.java | 102 ++++++++++++++---- 2 files changed, 85 insertions(+), 102 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 80c378a666..87e0aebb1c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -17,17 +17,9 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.SystemClock; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.TimestampAdjuster; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; -import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; @@ -41,7 +33,6 @@ import com.google.android.exoplayer2.trackselection.BaseTrackSelection; 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.UriUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -89,14 +80,6 @@ import java.util.Locale; } - private static final String AAC_FILE_EXTENSION = ".aac"; - private static final String AC3_FILE_EXTENSION = ".ac3"; - private static final String EC3_FILE_EXTENSION = ".ec3"; - private static final String MP3_FILE_EXTENSION = ".mp3"; - private static final String MP4_FILE_EXTENSION = ".mp4"; - private static final String VTT_FILE_EXTENSION = ".vtt"; - private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; - private final DataSource dataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsUrl[] variants; @@ -281,68 +264,10 @@ import java.util.Locale; if (previous != null && !switchingVariant) { startTimeUs = previous.getAdjustedEndTimeUs(); } - Format format = variants[newVariantIndex].format; - Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - // Set the extractor that will read the chunk. - Extractor extractor; - boolean needNewExtractor = previous == null - || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber - || format != previous.trackFormat; - boolean extractorNeedsInit = true; - TimestampAdjuster timestampAdjuster = null; - String lastPathSegment = chunkUri.getLastPathSegment(); - if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { - // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner - // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 - // case below. - extractor = new AdtsExtractor(startTimeUs); - } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { - extractor = new Ac3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) - || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { - timestampAdjuster = timestampAdjusterProvider.getAdjuster(segment.discontinuitySequenceNumber, - startTimeUs); - extractor = new WebvttExtractor(format.language, timestampAdjuster); - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { - if (needNewExtractor) { - timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); - } else { - extractorNeedsInit = false; - extractor = previous.extractor; - } - } else if (needNewExtractor) { - // MPEG-2 TS segments, but we need a new extractor. - timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultTsPayloadReaderFactory.Flags - int esReaderFactoryFlags = 0; - String codecs = format.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can - // explicitly ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; - } - } - extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); - } else { - // MPEG-2 TS segments, and we need to continue using the same extractor. - extractor = previous.extractor; - extractorNeedsInit = false; - } + TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( + segment.discontinuitySequenceNumber, startTimeUs); DataSpec initDataSpec = null; Segment initSegment = mediaPlaylist.initializationSegment; @@ -356,9 +281,9 @@ import java.util.Locale; DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - segment, chunkMediaSequence, isTimestampMaster, timestampAdjuster, extractor, - extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment, + chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, + encryptionIv); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 83343984b7..d0ad8d817f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -15,15 +15,23 @@ */ package com.google.android.exoplayer2.source.hls; +import android.text.TextUtils; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; 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.concurrent.atomic.AtomicInteger; @@ -35,6 +43,14 @@ import java.util.concurrent.atomic.AtomicInteger; private static final AtomicInteger UID_SOURCE = new AtomicInteger(); + private static final String AAC_FILE_EXTENSION = ".aac"; + private static final String AC3_FILE_EXTENSION = ".ac3"; + private static final String EC3_FILE_EXTENSION = ".ec3"; + private static final String MP3_FILE_EXTENSION = ".mp3"; + private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String VTT_FILE_EXTENSION = ".vtt"; + private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + /** * A unique identifier for the chunk. */ @@ -45,11 +61,6 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final int discontinuitySequenceNumber; - /** - * The extractor into which this chunk is being consumed. - */ - public final Extractor extractor; - /** * The url of the playlist from which this chunk was obtained. */ @@ -58,11 +69,11 @@ import java.util.concurrent.atomic.AtomicInteger; private final DataSource initDataSource; private final DataSpec initDataSpec; private final boolean isEncrypted; - private final boolean extractorNeedsInit; - private final boolean shouldSpliceIn; private final boolean isMasterTimestampSource; private final TimestampAdjuster timestampAdjuster; + private final HlsMediaChunk previousChunk; + private Extractor extractor; private int initSegmentBytesLoaded; private int bytesLoaded; private boolean initLoadCompleted; @@ -82,19 +93,14 @@ import java.util.concurrent.atomic.AtomicInteger; * @param chunkIndex The media sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. - * @param extractor The extractor to decode samples from the data. - * @param extractorNeedsInit Whether the extractor needs initializing with the target - * {@link HlsSampleStreamWrapper}. - * @param shouldSpliceIn Whether the samples parsed from this chunk should be spliced into any - * samples already queued to the {@link HlsSampleStreamWrapper}. + * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment, int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, - Extractor extractor, boolean extractorNeedsInit, boolean shouldSpliceIn, byte[] encryptionKey, - byte[] encryptionIv) { + HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, segment.startTimeUs, segment.startTimeUs + segment.durationUs, chunkIndex); @@ -102,9 +108,7 @@ import java.util.concurrent.atomic.AtomicInteger; this.hlsUrl = hlsUrl; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - this.extractor = extractor; - this.extractorNeedsInit = extractorNeedsInit; - this.shouldSpliceIn = shouldSpliceIn; + this.previousChunk = previousChunk; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; initDataSource = dataSource; @@ -121,10 +125,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { extractorOutput = output; - output.init(uid, shouldSpliceIn); - if (extractorNeedsInit) { - extractor.init(output); - } + output.init(uid, previousChunk != null && previousChunk.hlsUrl != hlsUrl); } /** @@ -165,6 +166,9 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { + if (extractor == null) { + extractor = buildExtractor(); + } maybeLoadInitData(); if (!loadCanceled) { loadMedia(); @@ -173,8 +177,62 @@ import java.util.concurrent.atomic.AtomicInteger; // Private methods. + private Extractor buildExtractor() { + // Set the extractor that will read the chunk. + Extractor extractor; + boolean needNewExtractor = previousChunk == null + || previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber + || trackFormat != previousChunk.trackFormat; + boolean usingNewExtractor = true; + String lastPathSegment = dataSpec.uri.getLastPathSegment(); + if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner + // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 + // case below. + extractor = new AdtsExtractor(startTimeUs); + } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) + || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { + extractor = new Ac3Extractor(startTimeUs); + } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { + extractor = new Mp3Extractor(startTimeUs); + } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); + } else if (!needNewExtractor) { + // Only reuse TS and fMP4 extractors. + usingNewExtractor = false; + extractor = previousChunk.extractor; + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { + extractor = new FragmentedMp4Extractor(0, timestampAdjuster); + } else { + // MPEG-2 TS segments, but we need a new extractor. + // This flag ensures the change of pid between streams does not affect the sample queues. + @DefaultTsPayloadReaderFactory.Flags + int esReaderFactoryFlags = 0; + String codecs = trackFormat.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } + extractor = new TsExtractor(timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); + } + if (usingNewExtractor) { + extractor.init(extractorOutput); + } + return extractor; + } + private void maybeLoadInitData() throws IOException, InterruptedException { - if (!extractorNeedsInit || initLoadCompleted || initDataSpec == null) { + if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted + || initDataSpec == null) { return; } DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); From e56cf4903828c9144bcd75502b203ecd93930ca8 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Nov 2016 10:43:02 -0800 Subject: [PATCH 136/206] Fix BuildConfig generation for internal builds ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140210128 --- demo/build.gradle | 18 ++++++------------ .../exoplayer2/demo/DemoApplication.java | 4 +--- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/demo/build.gradle b/demo/build.gradle index 2c01cbbe73..27180682fa 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -37,23 +37,17 @@ android { abortOnError false } - flavorDimensions "extensions" - productFlavors { - noExtns { - dimension "extensions" - } - extns { - dimension "extensions" - } + noExtensions + withExtensions } } dependencies { compile project(':library') - extnsCompile project(path: ':extension-ffmpeg') - extnsCompile project(path: ':extension-flac') - extnsCompile project(path: ':extension-opus') - extnsCompile project(path: ':extension-vp9') + withExtensionsCompile project(path: ':extension-ffmpeg') + withExtensionsCompile project(path: ':extension-flac') + withExtensionsCompile project(path: ':extension-opus') + withExtensionsCompile project(path: ':extension-vp9') } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index a7e67d169a..b5db4c018d 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -46,9 +46,7 @@ public class DemoApplication extends Application { } public boolean useExtensionRenderers() { - // We should return BuildConfig.FLAVOR_extensions.equals("extns") here, but this is currently - // incompatible with a Google internal build system. - return true; + return BuildConfig.FLAVOR.equals("withExtensions"); } } From f9c7343e767df93f4ec3f7ef5997e5c235241e0e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Nov 2016 00:45:12 -0800 Subject: [PATCH 137/206] Add basic unit test for ExoPlayer/ExoPlayerImplInternal. The simple verifications in this test may be useful for smoke testing, but the coverage of ExoPlayerImplInternal is low. The intention is to add tests for more complex logic in ExoPlayerImplInternal in later changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140327358 --- .../android/exoplayer2/ExoPlayerTest.java | 443 ++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java new file mode 100644 index 0000000000..cec8fb606e --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2016 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; + +import android.os.Handler; +import android.os.HandlerThread; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.TestCase; + +/** + * Unit test for {@link ExoPlayer}. + */ +public final class ExoPlayerTest extends TestCase { + + /** + * For tests that rely on the player transitioning to the ended state, the duration in + * milliseconds after starting the player before the test will time out. This is to catch cases + * where the player under test is not making progress, in which case the test should fail. + */ + private static final int TIMEOUT_MS = 10000; + + public void testPlayToEnd() throws Exception { + PlayerWrapper playerWrapper = new PlayerWrapper(); + Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); + playerWrapper.setup(new SinglePeriodTimeline(0, false), new Object(), format); + playerWrapper.blockUntilEndedOrError(TIMEOUT_MS); + } + + /** + * Wraps a player with its own handler thread. + */ + private static final class PlayerWrapper implements ExoPlayer.EventListener { + + private final CountDownLatch endedCountDownLatch; + private final HandlerThread playerThread; + private final Handler handler; + + private Timeline expectedTimeline; + private Object expectedManifest; + private Format expectedFormat; + private ExoPlayer player; + private Exception exception; + private boolean seenPositionDiscontinuity; + + public PlayerWrapper() { + endedCountDownLatch = new CountDownLatch(1); + playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + handler = new Handler(playerThread.getLooper()); + } + + // Called on the test thread. + + public void blockUntilEndedOrError(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out."); + } + release(); + + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + } + + public void setup(final Timeline timeline, final Object manifest, final Format format) { + expectedTimeline = timeline; + expectedManifest = manifest; + expectedFormat = format; + handler.post(new Runnable() { + @Override + public void run() { + try { + Renderer fakeRenderer = new FakeVideoRenderer(expectedFormat); + player = ExoPlayerFactory.newInstance(new Renderer[] {fakeRenderer}, + new DefaultTrackSelector()); + player.addListener(PlayerWrapper.this); + player.setPlayWhenReady(true); + player.prepare(new FakeMediaSource(timeline, manifest, format)); + } catch (Exception e) { + handlePlayerException(e); + } + } + }); + } + + public void release() throws InterruptedException { + handler.post(new Runnable() { + @Override + public void run() { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handlePlayerException(e); + } finally { + playerThread.quit(); + } + } + }); + playerThread.join(); + } + + private void handlePlayerException(Exception exception) { + if (this.exception == null) { + this.exception = exception; + } + endedCountDownLatch.countDown(); + } + + // ExoPlayer.EventListener implementation. + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + assertEquals(expectedTimeline, timeline); + assertEquals(expectedManifest, manifest); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, + TrackSelectionArray trackSelections) { + assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + this.exception = exception; + endedCountDownLatch.countDown(); + } + + @Override + public void onPositionDiscontinuity() { + assertFalse(seenPositionDiscontinuity); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(0, player.getCurrentPeriodIndex()); + assertEquals(0, player.getCurrentPosition()); + assertEquals(0, player.getBufferedPosition()); + assertEquals(expectedTimeline, player.getCurrentTimeline()); + assertEquals(expectedManifest, player.getCurrentManifest()); + seenPositionDiscontinuity = true; + } + + } + + /** + * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating + * the period will return a {@link FakeMediaPeriod}. + */ + private static final class FakeMediaSource implements MediaSource { + + private final Timeline timeline; + private final Object manifest; + private final Format format; + + private FakeMediaPeriod mediaPeriod; + private boolean preparedSource; + private boolean releasedPeriod; + private boolean releasedSource; + + public FakeMediaSource(Timeline timeline, Object manifest, Format format) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + this.timeline = timeline; + this.manifest = manifest; + this.format = format; + } + + @Override + public void prepareSource(Listener listener) { + assertFalse(preparedSource); + preparedSource = true; + listener.onSourceInfoRefreshed(timeline, manifest); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + assertTrue(preparedSource); + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + assertTrue(preparedSource); + assertNull(mediaPeriod); + assertFalse(releasedPeriod); + assertFalse(releasedSource); + assertEquals(0, index); + assertEquals(0, positionUs); + mediaPeriod = new FakeMediaPeriod(format); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + assertTrue(preparedSource); + assertNotNull(this.mediaPeriod); + assertFalse(releasedPeriod); + assertFalse(releasedSource); + assertEquals(this.mediaPeriod, mediaPeriod); + this.mediaPeriod.release(); + releasedPeriod = true; + } + + @Override + public void releaseSource() { + assertTrue(preparedSource); + assertNotNull(this.mediaPeriod); + assertTrue(releasedPeriod); + assertFalse(releasedSource); + releasedSource = true; + } + + } + + /** + * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that + * track will give the player a {@link FakeSampleStream}. + */ + private static final class FakeMediaPeriod implements MediaPeriod { + + private final TrackGroup trackGroup; + + private boolean preparedPeriod; + + public FakeMediaPeriod(Format format) { + trackGroup = new TrackGroup(format); + } + + public void release() { + preparedPeriod = false; + } + + @Override + public void prepare(Callback callback) { + assertFalse(preparedPeriod); + preparedPeriod = true; + callback.onPrepared(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + assertTrue(preparedPeriod); + } + + @Override + public TrackGroupArray getTrackGroups() { + assertTrue(preparedPeriod); + return new TrackGroupArray(trackGroup); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + assertTrue(preparedPeriod); + assertEquals(1, selections.length); + assertEquals(1, mayRetainStreamFlags.length); + assertEquals(1, streams.length); + assertEquals(1, streamResetFlags.length); + assertEquals(0, positionUs); + if (streams[0] != null && (selections[0] == null || !mayRetainStreamFlags[0])) { + streams[0] = null; + } + if (streams[0] == null && selections[0] != null) { + FakeSampleStream stream = new FakeSampleStream(trackGroup.getFormat(0)); + assertEquals(trackGroup, selections[0].getTrackGroup()); + streams[0] = stream; + streamResetFlags[0] = true; + } + return 0; + } + + @Override + public long readDiscontinuity() { + assertTrue(preparedPeriod); + return C.TIME_UNSET; + } + + @Override + public long getBufferedPositionUs() { + assertTrue(preparedPeriod); + return C.TIME_END_OF_SOURCE; + } + + @Override + public long seekToUs(long positionUs) { + assertTrue(preparedPeriod); + assertEquals(0, positionUs); + return positionUs; + } + + @Override + public long getNextLoadPositionUs() { + assertTrue(preparedPeriod); + return 0; + } + + @Override + public boolean continueLoading(long positionUs) { + assertTrue(preparedPeriod); + return false; + } + + } + + /** + * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag + * on its input buffer. + */ + private static final class FakeSampleStream implements SampleStream { + + private final Format format; + + private boolean readFormat; + private boolean readEndOfStream; + + public FakeSampleStream(Format format) { + this.format = format; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + Assertions.checkState(!readEndOfStream); + if (readFormat) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + readEndOfStream = true; + return C.RESULT_BUFFER_READ; + } + formatHolder.format = format; + readFormat = true; + return C.RESULT_FORMAT_READ; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + // Do nothing. + } + + } + + /** + * Fake {@link Renderer} that supports any video format. The renderer verifies that it reads a + * given {@link Format} then a buffer with the end of stream flag set. + */ + private static final class FakeVideoRenderer extends BaseRenderer { + + private final Format expectedFormat; + + private boolean isEnded; + + public FakeVideoRenderer(Format expectedFormat) { + super(C.TRACK_TYPE_VIDEO); + Assertions.checkArgument(MimeTypes.isVideo(expectedFormat.sampleMimeType)); + this.expectedFormat = expectedFormat; + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (isEnded) { + return; + } + + // Verify the format matches the expected format. + FormatHolder formatHolder = new FormatHolder(); + readSource(formatHolder, null); + assertEquals(expectedFormat, formatHolder.format); + + // Verify that we get an end-of-stream buffer. + DecoderInputBuffer buffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + readSource(null, buffer); + assertTrue(buffer.isEndOfStream()); + isEnded = true; + } + + @Override + public boolean isReady() { + return isEnded; + } + + @Override + public boolean isEnded() { + return isEnded; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return MimeTypes.isVideo(format.sampleMimeType) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + } + + } + +} From f7132a7a7393668b32d657efb374db7e05ba4ca1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Nov 2016 01:57:13 -0800 Subject: [PATCH 138/206] Fix compiler warning about assignment in conditional. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140332890 --- extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index 0d083a8bd4..fa615f2ec1 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -267,7 +267,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, sampleFormat, 1); AVAudioResampleContext *resampleContext; if (context->opaque) { - resampleContext = (AVAudioResampleContext *)context->opaque; + resampleContext = (AVAudioResampleContext *) context->opaque; } else { resampleContext = avresample_alloc_context(); av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0); @@ -326,7 +326,7 @@ void releaseContext(AVCodecContext *context) { return; } AVAudioResampleContext *resampleContext; - if (resampleContext = (AVAudioResampleContext *)context->opaque) { + if ((resampleContext = (AVAudioResampleContext *) context->opaque)) { avresample_free(&resampleContext); context->opaque = NULL; } From 73220be19b95a27f16096b3e907aec86be758038 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 28 Nov 2016 03:15:20 -0800 Subject: [PATCH 139/206] drainOutputBuffer return false on EOS I can't see how this would ever make a difference, but there's no point in returning true. Either we've really reached EOS (in which case outputStreamEnded will be true and the next drainOutputBuffer will be turned into a no-op) or we've re-initialized the codec (in which case there wont be anything to drain since we wont have fed anything to the codec yet). This change should also prevent the hypothetical NPE described in issue #2096, although we're unsure how that NPE would occur unless MediaCodecRenderer has been extended in an unusual way. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140338581 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index ca06a00619..440451c183 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -479,7 +479,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codec != null) { TraceUtil.beginSection("drainAndFeed"); while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer()) {} + if (codec != null) { + while (feedInputBuffer()) {} + } TraceUtil.endSection(); } else if (format != null) { skipToKeyframeBefore(positionUs); @@ -864,7 +866,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // The dequeued buffer indicates the end of the stream. Process it immediately. processEndOfStream(); outputIndex = C.INDEX_UNSET; - return true; + return false; } else { // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be // processed by calling processOutputBuffer (possibly multiple times) below. @@ -885,7 +887,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codecNeedsEosPropagationWorkaround && (inputStreamEnded || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) { processEndOfStream(); - return true; } return false; } From eb81da7f2d503fa06f26fe4aed5d617d1f9177e2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 28 Nov 2016 09:59:05 -0800 Subject: [PATCH 140/206] Remove unnecessary layer in view hierarchy ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140369506 --- library/src/main/res/layout/exo_simple_player_view.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index b21b0d2bd6..1f59b7796d 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -13,9 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + From 2550362fa2f47b57ca710beeefbb48113b7c9156 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 28 Nov 2016 10:35:56 -0800 Subject: [PATCH 141/206] Clean up playback controls xml a little ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140374149 --- .../res/layout/exo_playback_control_view.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index 2cf8b132ac..1ea6b3a582 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -51,17 +51,18 @@ From 2753664c6bc7427511d6f1b812008610e2732a23 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Mon, 28 Nov 2016 14:14:26 -0800 Subject: [PATCH 142/206] Fixed an error in the date and time parser's handling of time zones. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140400905 --- .../java/com/google/android/exoplayer2/util/UtilTest.java | 2 ++ .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 8a5a8b3d0e..35e168e514 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -142,6 +142,8 @@ public class UtilTest extends TestCase { public void testParseXsDateTime() throws Exception { assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); } public void testUnescapeInvalidFileName() { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index cbaebc2b55..8278bc7b15 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -95,7 +95,7 @@ public final class Util { private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); + + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); From 7bbe213e578a3f2de8bc7e9b4886c8d5e74a8dda Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Nov 2016 07:41:58 -0800 Subject: [PATCH 143/206] Add option to select track that exceeds renderer capabilities Leaving disabled by default for now, but we may want to consider enabling it by default. Note that in V1 for ExtractorSampleSource the behaviour was equivalent to this option being enabled, since we didn't perform capabilities checks. For DASH/SS/HLS the V1 behaviour was equivalent to this option being disabled. The type in PlayerActivity has been changed just to make it easier to add a line that calls setParameters. Issue: #2034 Issue: #2007 Issue: #2078 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140477568 --- .../exoplayer2/demo/PlayerActivity.java | 3 +- .../trackselection/DefaultTrackSelector.java | 196 +++++++++++------- 2 files changed, 126 insertions(+), 73 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 413157cbd0..b3cc8c0ba5 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -55,7 +55,6 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -109,7 +108,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; - private MappingTrackSelector trackSelector; + private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; private boolean playerNeedsSource; diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 02c2defdfc..bf139c85a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -55,6 +55,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int maxVideoWidth; public final int maxVideoHeight; public final boolean exceedVideoConstraintsIfNecessary; + public final boolean exceedRendererCapabilitiesIfNecessary; public final int viewportWidth; public final int viewportHeight; public final boolean orientationMayChange; @@ -67,13 +68,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

  • Adaptation between different mime types is not allowed.
  • *
  • Non seamless adaptation is allowed.
  • *
  • No max limit for video width/height.
  • - *
  • Video constraints are ignored if no supported selection can be made otherwise.
  • + *
  • Video constraints are exceeded if no supported selection can be made otherwise.
  • + *
  • Renderer capabilities are not exceeded even if no supported selection can be made.
  • *
  • No viewport width/height constraints are set.
  • * */ public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, Integer.MAX_VALUE, - Integer.MAX_VALUE, true); + this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, false, + Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** @@ -86,8 +88,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. * @param maxVideoWidth Maximum allowed video width. * @param maxVideoHeight Maximum allowed video height. - * @param exceedVideoConstraintsIfNecessary True to ignore video constraints when no selections - * can be made otherwise. False to force constraints anyway. + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. * @param viewportWidth Viewport width in pixels. * @param viewportHeight Viewport height in pixels. * @param orientationMayChange Whether orientation may change during playback. @@ -95,7 +99,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, boolean exceedVideoConstraintsIfNecessary, - int viewportWidth, int viewportHeight, boolean orientationMayChange) { + boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, + boolean orientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; @@ -103,6 +108,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.orientationMayChange = orientationMayChange; @@ -124,7 +130,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, orientationMayChange); + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -140,9 +147,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -156,9 +163,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -172,9 +179,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -189,9 +196,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -216,8 +223,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Returns a {@link Parameters} instance with the provided * {@code exceedVideoConstraintsIfNecessary} value. * - * @param exceedVideoConstraintsIfNecessary True to ignore video constraints when no selections - * can be made otherwise. False to force constraints anyway. + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. * @return A {@link Parameters} instance with the provided * {@code exceedVideoConstraintsIfNecessary} value. */ @@ -227,9 +234,29 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); + } + + /** + * Returns a {@link Parameters} instance with the provided + * {@code exceedRendererCapabilitiesIfNecessary} value. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. + * @return A {@link Parameters} instance with the provided + * {@code exceedRendererCapabilitiesIfNecessary} value. + */ + public Parameters withExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -247,9 +274,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, orientationMayChange); } /** @@ -289,6 +316,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary + && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && orientationMayChange == other.orientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) @@ -304,6 +332,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); + result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (orientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; @@ -319,9 +348,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f; private static final int[] NO_TRACKS = new int[0]; + private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; - private final AtomicReference params; + private final AtomicReference paramsReference; /** * Constructs an instance that does not support adaptive video. @@ -338,7 +368,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public DefaultTrackSelector(TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; - params = new AtomicReference<>(new Parameters()); + paramsReference = new AtomicReference<>(new Parameters()); } /** @@ -347,8 +377,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param params The parameters for track selection. */ public void setParameters(Parameters params) { - if (!this.params.get().equals(params)) { - this.params.set(Assertions.checkNotNull(params)); + Assertions.checkNotNull(params); + if (!paramsReference.getAndSet(params).equals(params)) { invalidate(); } } @@ -359,7 +389,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The current selection parameters. */ public Parameters getParameters() { - return params.get(); + return paramsReference.get(); } // MappingTrackSelector implementation. @@ -370,7 +400,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { throws ExoPlaybackException { // Make a track selection for each renderer. TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCapabilities.length]; - Parameters params = this.params.get(); + Parameters params = paramsReference.get(); for (int i = 0; i < rendererCapabilities.length; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: @@ -379,20 +409,23 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.maxVideoHeight, params.allowNonSeamlessAdaptiveness, params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, params.orientationMayChange, adaptiveVideoTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary); + params.exceedVideoConstraintsIfNecessary, + params.exceedRendererCapabilitiesIfNecessary); break; case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage); + rendererFormatSupports[i], params.preferredAudioLanguage, + params.exceedRendererCapabilitiesIfNecessary); break; case C.TRACK_TYPE_TEXT: rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage); + params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i]); + rendererTrackGroupArrays[i], rendererFormatSupports[i], + params.exceedRendererCapabilitiesIfNecessary); break; } } @@ -406,7 +439,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, int viewportHeight, boolean orientationMayChange, TrackSelection.Factory adaptiveVideoTrackSelectionFactory, - boolean exceedConstraintsIfNecessary) throws ExoPlaybackException { + boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) + throws ExoPlaybackException { TrackSelection selection = null; if (adaptiveVideoTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, @@ -416,7 +450,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary); + viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary, + exceedRendererCapabilitiesIfNecessary); } return selection; } @@ -512,7 +547,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight) { - return isSupported(formatSupport) && ((formatSupport & requiredAdaptiveSupport) != 0) + return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); @@ -520,37 +555,44 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int viewportWidth, - int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary) { + int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; + int selectedTrackScore = 0; int selectedPixelCount = Format.NO_VALUE; - boolean selectedIsWithinConstraints = false; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { - TrackGroup group = groups.get(groupIndex); - List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth, - viewportHeight, orientationMayChange); + TrackGroup trackGroup = groups.get(groupIndex); + List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, + viewportWidth, viewportHeight, orientationMayChange); int[] trackFormatSupport = formatSupport[groupIndex]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { - Format format = group.getFormat(trackIndex); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); - int pixelCount = format.getPixelCount(); - boolean selectTrack; - if (selectedIsWithinConstraints) { - selectTrack = isWithinConstraints - && comparePixelCounts(pixelCount, selectedPixelCount) > 0; - } else { - selectTrack = isWithinConstraints || (exceedConstraintsIfNecessary - && (selectedGroup == null - || comparePixelCounts(pixelCount, selectedPixelCount) < 0)); + if (!isWithinConstraints && !exceedConstraintsIfNecessary) { + // Track should not be selected. + continue; + } + int trackScore = isWithinConstraints ? 2 : 1; + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } + boolean selectTrack = trackScore > selectedTrackScore; + if (trackScore == selectedTrackScore) { + // Use the pixel count as a tie breaker. If we're within constraints prefer a higher + // pixel count, else prefer a lower count. If still tied then prefer the first track + // (i.e. the one that's already selected). + int pixelComparison = comparePixelCounts(format.getPixelCount(), selectedPixelCount); + selectTrack = isWithinConstraints ? pixelComparison > 0 : pixelComparison < 0; } if (selectTrack) { - selectedGroup = group; + selectedGroup = trackGroup; selectedTrackIndex = trackIndex; - selectedPixelCount = pixelCount; - selectedIsWithinConstraints = isWithinConstraints; + selectedTrackScore = trackScore; + selectedPixelCount = format.getPixelCount(); } } } @@ -577,7 +619,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio track selection implementation. protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage) { + String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -585,7 +627,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore; @@ -600,6 +642,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } else { trackScore = 1; } + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } if (trackScore > selectedTrackScore) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; @@ -615,7 +660,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text track selection implementation. protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredTextLanguage, String preferredAudioLanguage) { + String preferredTextLanguage, String preferredAudioLanguage, + boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -623,7 +669,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; @@ -648,7 +694,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { trackScore = 1; } } else { - trackScore = 0; + // Track should not be selected. + continue; + } + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } if (trackScore > selectedTrackScore) { selectedGroup = trackGroup; @@ -665,7 +715,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport) { + int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -673,10 +723,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore = isDefault ? 2 : 1; + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } if (trackScore > selectedTrackScore) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; @@ -689,12 +742,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } - private static boolean isSupported(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; + protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { + int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities + && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - private static boolean formatHasLanguage(Format format, String language) { + protected static boolean formatHasLanguage(Format format, String language) { return language != null && language.equals(Util.normalizeLanguageCode(format.language)); } From bfc2faa269aba3e006b5da547e7d0fbfc1033b6d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Nov 2016 07:59:49 -0800 Subject: [PATCH 144/206] Enable focus highlighting in track selection dialogs ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140479099 --- .../exoplayer2/demo/TrackSelectionHelper.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 8892c138d0..936cdf90f8 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -18,7 +18,9 @@ package com.google.android.exoplayer2.demo; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; +import android.content.res.TypedArray; import android.text.TextUtils; import android.util.Pair; import android.view.LayoutInflater; @@ -100,7 +102,7 @@ import java.util.Locale; AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title) - .setView(buildView(LayoutInflater.from(builder.getContext()))) + .setView(buildView(builder.getContext())) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, null) .create() @@ -108,13 +110,20 @@ import java.util.Locale; } @SuppressLint("InflateParams") - private View buildView(LayoutInflater inflater) { + private View buildView(Context context) { + LayoutInflater inflater = LayoutInflater.from(context); View view = inflater.inflate(R.layout.track_selection_dialog, null); ViewGroup root = (ViewGroup) view.findViewById(R.id.root); + TypedArray attributeArray = context.getTheme().obtainStyledAttributes( + new int[] {android.R.attr.selectableItemBackground}); + int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0); + attributeArray.recycle(); + // View for disabling the renderer. disableView = (CheckedTextView) inflater.inflate( android.R.layout.simple_list_item_single_choice, root, false); + disableView.setBackgroundResource(selectableItemBackgroundResourceId); disableView.setText(R.string.selection_disabled); disableView.setFocusable(true); disableView.setOnClickListener(this); @@ -123,6 +132,7 @@ import java.util.Locale; // View for clearing the override to allow the selector to use its default selection logic. defaultView = (CheckedTextView) inflater.inflate( android.R.layout.simple_list_item_single_choice, root, false); + defaultView.setBackgroundResource(selectableItemBackgroundResourceId); defaultView.setText(R.string.selection_default); defaultView.setFocusable(true); defaultView.setOnClickListener(this); @@ -146,6 +156,7 @@ import java.util.Locale; : android.R.layout.simple_list_item_single_choice; CheckedTextView trackView = (CheckedTextView) inflater.inflate( trackViewLayoutId, root, false); + trackView.setBackgroundResource(selectableItemBackgroundResourceId); trackView.setText(buildTrackName(group.getFormat(trackIndex))); if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED) { @@ -169,6 +180,7 @@ import java.util.Locale; // View for using random adaptation. enableRandomAdaptationView = (CheckedTextView) inflater.inflate( android.R.layout.simple_list_item_multiple_choice, root, false); + enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId); enableRandomAdaptationView.setText(R.string.enable_random_adaptation); enableRandomAdaptationView.setOnClickListener(this); root.addView(inflater.inflate(R.layout.list_divider, root, false)); From 91c58627be09cb0bc779d43778a55cd5ce76484f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Nov 2016 09:33:10 -0800 Subject: [PATCH 145/206] Fix lint errors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140488486 --- .../google/android/exoplayer2/metadata/id3/Id3Decoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index b71828c7fc..d27c4f06e9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -368,13 +368,13 @@ public final class Id3Decoder implements MetadataDecoder { int mimeTypeEndIndex; if (majorVersion == 2) { mimeTypeEndIndex = 2; - mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase(); + mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); if (mimeType.equals("image/jpg")) { mimeType = "image/jpeg"; } } else { mimeTypeEndIndex = indexOfZeroByte(data, 0); - mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase(); + mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1")); if (mimeType.indexOf('/') == -1) { mimeType = "image/" + mimeType; } From 501f54a8a640ce799353ab001d67ed761834345e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Sat, 8 Oct 2016 14:16:37 +0100 Subject: [PATCH 146/206] Add #EXT-X-PROGRAM-DATE-TIME support for HLS media playlists Issue:#747 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140525595 --- .../exoplayer2/source/hls/HlsChunkSource.java | 26 +++---- .../exoplayer2/source/hls/HlsMediaChunk.java | 21 +++--- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +- .../source/hls/playlist/HlsMediaPlaylist.java | 54 ++++++-------- .../hls/playlist/HlsPlaylistParser.java | 23 +++--- .../hls/playlist/HlsPlaylistTracker.java | 73 +++++++------------ 6 files changed, 85 insertions(+), 116 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 87e0aebb1c..a8c1ab15f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -212,7 +212,8 @@ import java.util.Locale; // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true, + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, + targetPositionUs - mediaPlaylist.startTimeUs, true, !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { // We try getting the next chunk without adapting in case that's the reason for falling @@ -259,16 +260,6 @@ import java.util.Locale; clearEncryptionData(); } - // Compute start time and sequence number of the next chunk. - long startTimeUs = segment.startTimeUs; - if (previous != null && !switchingVariant) { - startTimeUs = previous.getAdjustedEndTimeUs(); - } - Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - - TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - DataSpec initDataSpec = null; Segment initSegment = mediaPlaylist.initializationSegment; if (initSegment != null) { @@ -277,13 +268,20 @@ import java.util.Locale; initSegment.byterangeLength, null); } + // Compute start time of the next chunk. + long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( + segment.discontinuitySequenceNumber, startTimeUs); + // Configure the data source and spec for the chunk. + Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment, - chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, - encryptionIv); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, + segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous, + encryptionKey, encryptionIv); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index d0ad8d817f..0edf39ccba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; @@ -89,8 +88,10 @@ import java.util.concurrent.atomic.AtomicInteger; * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. - * @param segment The {@link Segment} for which this media chunk is created. + * @param startTimeUs The start time of the chunk in microseconds. + * @param endTimeUs The end time of the chunk in microseconds. * @param chunkIndex The media sequence number of the chunk. + * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. @@ -98,21 +99,21 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment, - int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, + HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, + long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, + boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, segment.startTimeUs, - segment.startTimeUs + segment.durationUs, chunkIndex); + trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.previousChunk = previousChunk; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; initDataSource = dataSource; - discontinuitySequenceNumber = segment.discontinuitySequenceNumber; adjustedEndTimeUs = endTimeUs; uid = UID_SOURCE.getAndIncrement(); } @@ -136,7 +137,7 @@ import java.util.concurrent.atomic.AtomicInteger; } /** - * Returns the presentation time in microseconds of the last sample in the chunk + * Returns the presentation time in microseconds of the last sample in the chunk. */ public long getAdjustedEndTimeUs() { return adjustedEndTimeUs; @@ -231,8 +232,8 @@ import java.util.concurrent.atomic.AtomicInteger; } private void maybeLoadInitData() throws IOException, InterruptedException { - if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted - || initDataSpec == null) { + if ((previousChunk != null && previousChunk.extractor == extractor) + || initLoadCompleted || initDataSpec == null) { return; } DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index f418bbded3..a5ae09d2fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -103,10 +103,10 @@ public final class HlsMediaSource implements MediaSource, SinglePeriodTimeline timeline; if (playlistTracker.isLive()) { // TODO: fix windowPositionInPeriodUs when playlist is empty. - long windowPositionInPeriodUs = playlist.getStartTimeUs(); + long windowPositionInPeriodUs = playlist.startTimeUs; List segments = playlist.segments; long windowDefaultStartPositionUs = segments.isEmpty() ? 0 - : segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs; + : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); } else /* not live */ { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 2962d656be..41ea2a03b9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.C; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,7 +32,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final String url; public final long durationUs; public final int discontinuitySequenceNumber; - public final long startTimeUs; + public final long relativeStartTimeUs; public final boolean isEncrypted; public final String encryptionKeyUri; public final String encryptionIV; @@ -45,12 +44,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } public Segment(String uri, long durationUs, int discontinuitySequenceNumber, - long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, + long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = uri; this.durationUs = durationUs; this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.startTimeUs = startTimeUs; + this.relativeStartTimeUs = relativeStartTimeUs; this.isEncrypted = isEncrypted; this.encryptionKeyUri = encryptionKeyUri; this.encryptionIV = encryptionIV; @@ -59,64 +58,55 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(Long startTimeUs) { - return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); - } - - public Segment copyWithStartTimeUs(long startTimeUs) { - return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted, - encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); + public int compareTo(Long relativeStartTimeUs) { + return this.relativeStartTimeUs > relativeStartTimeUs + ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } } + public final long startTimeUs; public final int mediaSequence; public final int version; public final Segment initializationSegment; public final List segments; public final boolean hasEndTag; + public final boolean hasProgramDateTime; public final long durationUs; - public HlsMediaPlaylist(String baseUri, int mediaSequence, int version, - boolean hasEndTag, Segment initializationSegment, List segments) { + public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version, + boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, + List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); + this.startTimeUs = startTimeUs; this.mediaSequence = mediaSequence; this.version = version; this.hasEndTag = hasEndTag; + this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { - Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + last.durationUs - first.startTimeUs; + durationUs = last.relativeStartTimeUs + last.durationUs; } else { durationUs = 0; } } - public long getStartTimeUs() { - return segments.isEmpty() ? 0 : segments.get(0).startTimeUs; + public boolean isNewerThan(HlsMediaPlaylist other) { + return other == null || mediaSequence > other.mediaSequence + || (mediaSequence == other.mediaSequence && segments.size() > other.segments.size()) + || (hasEndTag && !other.hasEndTag); } public long getEndTimeUs() { - return getStartTimeUs() + durationUs; + return startTimeUs + durationUs; } - public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) { - long startTimeOffsetUs = newStartTimeUs - getStartTimeUs(); - int segmentsSize = segments.size(); - List newSegments = new ArrayList<>(segmentsSize); - for (int i = 0; i < segmentsSize; i++) { - Segment segment = segments.get(i); - newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs)); - } - return copyWithSegments(newSegments); - } - - public HlsMediaPlaylist copyWithSegments(List segments) { - return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag, - initializationSegment, segments); + public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) { + return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag, + hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 420500615a..3829cbadbf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -43,6 +44,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser oldSegments = oldPlaylist.segments; + List oldSegments = oldPlaylist.segments; int oldPlaylistSize = oldSegments.size(); - int newPlaylistSize = newPlaylist.segments.size(); - int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; - if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0 - && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { + if (!newPlaylist.isNewerThan(oldPlaylist)) { // Playlist has not changed. return oldPlaylist; } - if (mediaSequenceOffset < 0) { - // Playlist has changed but media sequence has regressed. - return oldPlaylist; - } + int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; if (mediaSequenceOffset <= oldPlaylistSize) { - // We can extrapolate the start time of new segments from the segments of the old snapshot. - ArrayList newSegments = new ArrayList<>(newPlaylistSize); - for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) { - newSegments.add(oldSegments.get(i)); - } - HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); - for (int i = newSegments.size(); i < newPlaylistSize; i++) { - lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( - lastSegment.startTimeUs + lastSegment.durationUs); - newSegments.add(lastSegment); - } - return newPlaylist.copyWithSegments(newSegments); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize + ? oldPlaylist.getEndTimeUs() + : oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs; + return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs); } + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs); } /** @@ -375,31 +368,19 @@ public final class HlsPlaylistTracker implements Loader.Callback segments = new ArrayList<>(latestPlaylistSnapshot.segments); int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence; - if (indexOfChunk < 0) { + if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) { return; } - Segment actualSegment = segments.get(indexOfChunk); - long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs); + Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk); + long segmentAbsoluteStartTimeUs = + actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs; + long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs); if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { return; } - segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs)); - // Propagate the adjustment backwards. - for (int i = indexOfChunk - 1; i >= 0; i--) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs)); - } - // Propagate the adjustment forward. - int segmentsSize = segments.size(); - for (int i = indexOfChunk + 1; i < segmentsSize; i++) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs)); - } - latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments); + latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs( + adjustedStartTimeUs - actualSegment.relativeStartTimeUs); } // Loader.Callback implementation. From 257671467f6c97762f473c42709aecdd28d149bc Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 30 Nov 2016 06:23:47 -0800 Subject: [PATCH 147/206] Fix possible failure in CachedContentIndex encrypted cache index file read. Encryption key in index file is read by DataInputStream.read() which may return less bytes than required. Replaced it with readFully() which should read full length of data. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140597693 --- .../exoplayer2/upstream/cache/CachedContentIndex.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index d8224665b9..a4a97b4332 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; @@ -55,6 +56,8 @@ import javax.crypto.spec.SecretKeySpec; private static final int FLAG_ENCRYPTED_INDEX = 1; + private static final String TAG = "CachedContentIndex"; + private final HashMap keyToContent; private final SparseArray idToKey; private final AtomicFile atomicFile; @@ -224,7 +227,7 @@ import javax.crypto.spec.SecretKeySpec; return false; } byte[] initializationVector = new byte[16]; - input.read(initializationVector); + input.readFully(initializationVector); IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); try { cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); @@ -245,6 +248,7 @@ import javax.crypto.spec.SecretKeySpec; return false; } } catch (IOException e) { + Log.e(TAG, "Error reading cache content index file.", e); return false; } finally { if (input != null) { From 289ae3ff38280f56d5af3371024d21ecb2f9758c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Nov 2016 08:04:25 -0800 Subject: [PATCH 148/206] Make demo app visible in AndroidTV launcher ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140605251 --- demo/src/main/AndroidManifest.xml | 5 ++++- demo/src/main/res/drawable-xhdpi/ic_banner.png | Bin 0 -> 8662 bytes 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 demo/src/main/res/drawable-xhdpi/ic_banner.png diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1f015827c9..2378627ffa 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -21,12 +21,14 @@ - + + @@ -37,6 +39,7 @@ + diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demo/src/main/res/drawable-xhdpi/ic_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..dad9daa4deb9b55a862d94ff5cb97f18bc94b55d GIT binary patch literal 8662 zcmch7gdP-Ad{kGREBrH9{_L=!7E6q{}nCskqGa z;9X&jZ0-p=ZRZ^pB{pSwbZyC@y5-&Vsj~G*5)?lAgQ_&kU2v@?;C4JO*VwZak1}=SvvDCe!Ti7b!9m;SOq(UmmLCpke-eb< z&Fh?^2!#}?M?2r)Nn;^}!-WXny)3$4Uri@%vlW>$q;>8l*#FA8ARu&);W?)mx z-g9YJ6~55fNbPO&&9J8^D+?oH@MNnK_^bcHtHU-q5Dd`R6O2>L{F5>oQK!>egRH)WlMO6p4V&N+l64P*KW^>uYiRt=o#PJY}<=$-jspenMhLL=gWFPI9Iw| zBJ3$DHsgZ3r zN6i~0DR-jnlAt%nc2FV~Mt~dskQkzEG1FV2fI9a*j*ht&zY|yDOEPt0>V~fe(6BmL{d4>!>tp}5&u1V z)OL1mnZ4L%`jzsGoRM5F+g_f;QLCYJ@SEkBXLk_l`FSW}>GdUayO zb$59I+nGJh6@55aVV%>O@3+^@?7Dz41U9aW8>p`HnpW}!_#1|Lc7Yj_VZPr5D zQgexxeMrH5~~hcKAANRj%hleGyqKfNO&|&@55*4 z{Kd5g!^=ro+^R;#Jp_>Fm8`GZ9Fj zoK+$JrrisREpBhlyoiD$mI#2hByT>NgNDR-vUgcBP0OGz$1Dbdtdf;3Ht zPW;Evom??@odj_W=k9fSQj_;BY%+=E?mn4h_3#qF-@L}?O%5*Vdu=YTa z!iNSA|3m(%3_&aovQKWxF(rFYE2li zu&M#9Ly2ZWVdsKmE`%~oxQ1EuGBH_BrtW;MC#5xYk3tmHU=KTgi@&WTyz`grIWPcEs#EBsLcW#kAmP2@1Q z70Z#lO8_C@`}f{;w+Aw3_O@}-yC*Lo&{zsjP%r}E^`t1sg$%{J!_7OqpKuj9O}ZC3 zq6H;c|6@-0Qbc+4oh|Yu?uQ$faoj1tyEP*grf-luPX2v1YXHO$+ z9@R4jjkCXKUC*j*v>0Plt`bhBaC6A9@xp zJ<%Y`u7s;vy(k%D_>szIPx>;Ru`xv0t?jc*B!Gv>No{Q&iW?$zL~OuHIV zePU=6e~t~~x&XEDdqnBjg=3HxdR65?*8KqyB*H}BucjY1PPJ1C+-W`z?Xs3=Keb3e z+2f6P`n`3pA0@gF2x2DT>T6J6=J9ivkuRUcRpw8;>U%K7%%?@J?MVzo{Xz+vR|G+Op;`FOdLfKUA1& zldvT`jFBgGdZc#j(il!MPy)X|lxq!)ba!-6XU>x<*Avd=jsPHa=l}yqu^7g%(bxOD2ot zjw0`u+y}$1GlURN*RI?THM#aQ)0>F=ZOPEWfY5$spnrgdt_6*0Q(g%5`rkc?X46|M zzDPQmLK+6ANfwfJ_`CH|R(MuVt)+DXe&`loqUx!T8u`c@(ZgfSQx(`XxD&aTvblXw zJHVD431WKo>w1p`#k1~LFrbhc;uF?->BHK({cEv1{2Ai&;tzlQPRY18M!*lETrEvb z5X0Bo!p`tI-;a~BC<{L+@zF{t?p+=1 z?7j@>jACYFnI)Sdj{kB?6yY?kkUA%;a22?lz(0HC!8VE{BvU5it`n-o5_WL%>Iu_TEKHF3k~|d5 zLgivrSkKSfha=PY@aNr&ly^_hbF4P()BCI*V_Y^DR*lhZjAD3!2- zxi&@auTMfLAO1DK`nBMl*v3?I`vn}1wQe~voq)y#R*FzuLcOt37OWm9km-u{9k0=87wwOECA3pF&6su5S5bkX9`P80Hs< zl5&vVl#Ho(^-O?I<6*GPqetMWWo%p6x$toal1tL-7^}n#>*8M|qbyn;1%VoNygN)% zX}S6Hv{?Mu5a^o5&_ynCa{=}x)^*ZhccB-Y@XfPDE!CCONMh@W=Mu1O^o>h*COk?* zL^t8yS(;>%CcV^)`d!3d;JHPma=Nf~{kHvTF86fn1Pw@$JL4bRd^g;(0G};7S3%%?^z(J9@$k$m?}7MJKidKaa+EiH_w{n4<(C}^ttDC z4OXS%WE({c14~zsUSXsPWB~WJy|<|5FqB#F*)8b?-xV4_R$jE~bQRKE0>D@YvBgLj zRBg|st6rj#1C7L+*&GcyDIT_{`zEgQS;Z>iO6xl4=d2-j6`Rj+oQ2Kof25ET62Ct;Zl zAU#kzNo{WXXhA7&l)%GD5Dm#N4*z`%HmYQ2M7AvGcgq)<8!H4#ckN5KPBTqRIvL6t zgS#HSryxR_SCc$~Pk_(S{nO!_lJg1xeT`IA8F{B|G$8sVq3RPG>A$bLPb19fNsp&v z&N$-hZS-l%nA&-VOjHV%R*MQqy4VtxaiV+NVYcjGVEe%~{kuL3osAw9SpUQQw#A^N03J@faV(!Jp`9T~S4!^v`YDafY{7 z5#@r(A|C-I^naXvqI(J#Bh}Ky6p{03R?GK=SDP};9iXtAEsu>Mxe%P`-%$v5K>4~i z?+PpyfL)W*5i$P!)G>Sfsz8FrQB@vTIYzml8ym#ET-jf;s{K~fJT+KzuH4l_H)c2e zLwZR!SsWyeHckXe+rP=9`Q}ti+Bhw?Ct}brQ(i&8{Lrrrk3&py3akCGX+?=EX^QeU zWhZN|4R!R8P%w$#>J@wN<}E2=!%Z6lmzh7BBlN|zzmD~iSWHKDbeqf~;p(WYTJcAC z+5-z#AE})zUYMxUJ}a0ZNs z_U1Zj0jlBaMvu5T$5%HlW5u=0b^m!9h>jh9kd?hUO+oz4O~IfHT{R8VuQt}|qq^JNdti54E7=0n=oSj-N5^JG2c@E_?=Yzggzxc@`ZtsfU=oGD+mzxO{rM_eF)=92|~j~m?T@NyrYNz zum<#mS%|0LF|>dI{JZ*<>~%Hmb7z1}z=^xsjmxj&E;%2?eIq;gWzbFWG{c27e;K9+5s^d3g2R{lMqM}URVfEBKo?pOdQeiEIwK^{k zH?cFvY^Y?op+C>;X>t`RXcOiy>fGnZrZYUD$#jC5V@XbM30E8L;iN8rN>INk32TcWu3|tzPq>$jlv9{ zDoSi(w|MySE~d}FNW8CfuJ@X&8?y=_*9rUUC!9bX%5QNe^|B-Uq@d%yH~g#8wS}aje3M77GoU4cls80WF)J4t+>`HeRmlOgejXCyE)US{?b^xmq%J?8=DcdqM|? zLi&7@f-5KOJ{j4kSfQ%uCj5;K8BM=%Oi(s*%H3=0v7R>JR2%rI$ZEJkW)5IR6?#3c zgGL7g-*w)eA}Mt2pVK+;{l@IKivK$Q89#z8!23z1iJIdpE19JbQIqS%d4BEAY9Vid zT7;8?hMa1KT8`3@Rve_1w6^u$o#pKyuxRGy30xGB7TfN(d||bGHNh?!bkEj<38*R; zDs>s{VzG@A_j?X}16D-2V!~?<=7WD~P=y;M4!Du(+O{_sNR7c(sp*hq9)FVIe*)QD z>5ycQ=T}pFC=VMno%L_^4X@NlK+)VG-lk;lPq+@d`YZWqVbrn{xVkvXjlX+k{)V-< zTmU}%VOum#gjQ>?(}w8vzzp9jS58socDR|zJpF*kF?tn*N<0bf;=dZu;lt=qfJAWd zEQgThG*2ZTS`j@DcA@$$&6f1Gotm`l)-!Z7Vd(RmkY?Mpey^Dk|F-C1HoNc}%f!8= zgMDqL0*MQJeqh)0<-0v0>+Y=Mds(L2g$5!&9xPAMLq67Q(}9J=fYB~*EwYu|zN+ta zB{&S4J{GWn@g@B?%LB6mUeG;&jzFENAx650>F|G{Y4RPBQ{jYXZO%%SUttn=Y7b{qmIB?#^Ten zqd936KE5tBV+te4sAKmSB#Dc?tlBUb7(wO65t%2)`Z7{&!V1I`09%brk&EtXzl!z0 z9I(N{>P=w-T*)xFSE%{m_pZvi-d_fEwBuA6AHGSuEtMBr0@Wqz&7C|7e(6bEcu5k} zhInf861CBMFVuO3{1&&-Kde*4jpf-l^1_-&YAx+kcf_x4_@~{U6O+827sh{#9G`tL z2Yh?=@JhCigEY3o(I}>SYI4V9WrFfD0WilK`fi^JeT7+y(u7{~KDK`>W&lRWl9M%& zh$`e$^6xT&gnqi8Jhpr-9zJNb{k*zB-&~=3hTqVr|4iz&N=vzk!0#12J01&~c zg~`MQF`@B0z2Sa#$}z}qc-_^2SL=8kUv@u1IYG^_H9SpgO+N_ojQ^n8wpylm#`Ixp zxki2vt@YQluSC}RUbhm8J`%v~im7{QQ??sEl;59swUSbr9|XzxO{?i%h}RwBkk^nL zdoycDKx1nA=B=Ll!DY=z_kQS-XH{;4Q{~kc<{izu$O;>CKCg($SIPJ#v|FOefzoo| z9V~i#Pdkqo43yoUqnmX$H{3Fdo5?~cf&TbbHcljNQtGWlKp!oYJ!sxahK3l(+F`xO zcJIECo+W{rr(Jo|8u=pQOcaMy=AQEYymGDgIxz_FFU{sZ>oxadxALJ?sj)ROrJHjK z$*eXC9B#mo&Jv$`iv`gHT&41jrnXP0vM0-ips}Q~fR?(aeKcJ@LdQY75LyrKtp5}x z0RD=y+bFRyF?pAEH~!9KrHWWL3K3h6aB9r9uW!gutiwes53+oxPLaLfzaO_*N!+mx zEl!_SLFs`Jmr*@mFU;=szw6#^p`ImYQLSmG#YF6N7taz`9{6D#6>JJVrlG`S2{Acq4g zRKK=tfHBxE5^QQ5t8XMFI|#S&tx`q_TMI29kHF;J-h#k{#yMEpe8m3V-Q!gMk8kmiZQ{Wa7`^Pup+;n+ zn2HhnZCrxG10t)-DV-yV1tan;GvHR!YIXwI2pA zZ+*4C^-ZaY1Gv&cVqtAdk466bzQqe^A%04hUmkG)-D$l1qZ485~fvPLvcKYTjMf5+I31N;djdg0t=}8lg2uM*=e9p zAA`TjvRutx&t4|%fWss4d%SjpY=4**@XK!dO6y|W_=%4G#MT3i`^5E(bqgT)T4N%Q z5z%^JThmG)Y=78ArMZ@+IDCV1O-jJ9kb`*ZA)BTsOz4v`x>NTpZS4vdnDq0nod;E@ zQs+4^s-Owq`q>?w;24cJ6pWBK(_WNbCcKvdaeVI8dn1b2k;4tWcawa*%5vEb6BBs) z>Aqs%fH^n$)Gzc-qO^8gUKz(%Oi*EZ@S6bC=33IA)k6lPWV=c0xGI?l%e&!pJN>eU z#_I+>0-F3$iI+T7>wD!>VBq6O?-iYZvzy5>vq$+Xr=;yY;PZJ{#+^;Ch*rviGU|F< z@5dB=1+N=<-(&o-FEWWLun?x5?L_|12!BqXf2w5x{k{4&iYz&1x}*{q``F2OvT|A| z*2@`~S0{0+B(UAkL2fDqR$@^m$S!KrthT8eJhiTiIGMp3ugr@U&{kWyoqv2&+$?1= zaymR+yIG!PJrJ!l^AGBTjo%&V06K8j%?{z0wOm#`0Xbm6JRbvq6+oIV;N82* z5K16t(~nDDq{C;kc1s~dl*vyy&RS;5k&rcJ365ZW78u)R2p6RwP0)|dA97t6N(oqE}&4Vf8mR7MA43sLJLRv3f`a~*M6V#*3k{;LIGr6o#~(U>EaG7F-J z$|a=beP!@$fFYW$1&98x{&?qHEe*i77y(uSFa>j&w)Ff^{;u=APW|j)ZYqEdp@vuU z>xHvt6c9CehuM7Z#h2*ygjv8^1Sj6cnU@^+7al9~?`_?%I$!6KVfJ%ap|*dXT76|b ztc9EhWTu#EaK2m}3jaL(8%rIb5shRA2uYJ!t8?iS|6U2t`AV1?$w;3Rc@TmbG=cuK z3N2bw`wUygj`OKSS_YEYJ{_&(=CVSH+~m-iz=A3((dUidPEy{!>u^T{LentBb)OP9 n&ly%q*F)X^yTSsE-3O2!??~djE=&befdNoc(tcT^XdCfAvHE+^ literal 0 HcmV?d00001 From f702568776895531ebbe3dc8a49c0f4cf8fce122 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 30 Nov 2016 08:17:10 -0800 Subject: [PATCH 149/206] Skip tables with unexpected table_id for PAT and PMT readers ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140606435 --- .../extractor/ts/SectionReader.java | 12 ++++++---- .../exoplayer2/extractor/ts/TsExtractor.java | 23 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 9a181897ab..f78370dc69 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -44,16 +45,16 @@ public final class SectionReader implements TsPayloadReader { public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { reader.init(timestampAdjuster, extractorOutput, idGenerator); + sectionLength = C.LENGTH_UNSET; } @Override public void seek() { - // Do nothing. + sectionLength = C.LENGTH_UNSET; } @Override public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = data.readUnsignedByte(); data.skipBytes(pointerField); @@ -67,6 +68,9 @@ public final class SectionReader implements TsPayloadReader { sectionBytesRead = 0; sectionData.reset(sectionLength); + } else if (sectionLength == C.LENGTH_UNSET) { + // We're not already reading a section and this is not the start of a new one. + return; } int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); @@ -76,8 +80,8 @@ public final class SectionReader implements TsPayloadReader { // Not yet fully read. return; } - - if (Util.crc(sectionData.data, 0, sectionLength, 0xFFFFFFFF) != 0) { + sectionLength = C.LENGTH_UNSET; + if (Util.crc(sectionData.data, 0, sectionBytesRead, 0xFFFFFFFF) != 0) { // CRC Invalid. The section gets discarded. return; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 0403a970c8..4c5401f28b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -283,10 +283,15 @@ public final class TsExtractor implements Extractor { @Override public void consume(ParsableByteArray sectionData) { - // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x00 /* program_association_section */) { + // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. + return; + } + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), // section_number (8), last_section_number (8) - sectionData.skipBytes(8); + sectionData.skipBytes(7); int programCount = sectionData.bytesLeft() / 4; for (int i = 0; i < programCount; i++) { @@ -331,11 +336,15 @@ public final class TsExtractor implements Extractor { @Override public void consume(ParsableByteArray sectionData) { - // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), - // program_number (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) - // Skip the rest of the PMT header. - sectionData.skipBytes(10); + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x02 /* TS_program_map_section */) { + // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. + return; + } + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16), + // reserved (2), version_number (5), current_next_indicator (1), // section_number (8), + // last_section_number (8), reserved (3), PCR_PID (13) + sectionData.skipBytes(9); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); From 45c68a2fd55663addce79889c4a068b1f04983cf Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Nov 2016 08:46:49 -0800 Subject: [PATCH 150/206] Use separate mimeType for CEA-608 embedded in MP4 When CEA-608 is embedded in MP4 each packet consists of cc_data_1 and cc_data_2 only. The marker_bits, cc_valid and cc_type are implicit. As a result playback of CEA-608 embedded in MP4 broke when we started passing the extra byte for the TS case (and adjusted the decoder to assume the byte was present). This change introduces a special mimeType for the case where the byte is implicit (!). An alternative option was to insert the extra byte every 2 bytes in the MP4 extractor, but this is really quite fiddly to get right. Also made the loops in the 608/708 decoders robust against input of the wrong length. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140609304 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 3 ++- .../text/SubtitleDecoderFactory.java | 5 +++-- .../exoplayer2/text/cea/Cea608Decoder.java | 18 ++++++++++++------ .../android/exoplayer2/util/MimeTypes.java | 3 ++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 4272cdaa11..9dc0578263 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -621,8 +621,9 @@ import java.util.List; MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData, 0 /* subsample timing is absolute */); } else if (childAtomType == Atom.TYPE_c608) { + // Defined by the QuickTime File Format specification. out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, language, drmInitData); + MimeTypes.APPLICATION_MP4CEA608, null, Format.NO_VALUE, 0, language, drmInitData); out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index d1e474d434..a5d1c0a9c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -75,8 +75,8 @@ public interface SubtitleDecoderFactory { throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } if (clazz == Cea608Decoder.class) { - return clazz.asSubclass(SubtitleDecoder.class) - .getConstructor(Integer.TYPE).newInstance(format.accessibilityChannel); + return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE) + .newInstance(format.sampleMimeType, format.accessibilityChannel); } else { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); } @@ -102,6 +102,7 @@ public interface SubtitleDecoderFactory { case MimeTypes.APPLICATION_TX3G: return Class.forName("com.google.android.exoplayer2.text.tx3g.Tx3gDecoder"); case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder"); default: return null; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 5a2fc77c6c..2a12679a0b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleInputBuffer; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; /** @@ -52,6 +53,10 @@ public final class Cea608Decoder extends CeaDecoder { // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; + // An implied first byte for packets that are only 2 bytes long, consisting of marker bits + // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00). + private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC; + /** * Command initiating pop-on style captioning. Subsequent data should be loaded into a * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received, @@ -164,9 +169,8 @@ public final class Cea608Decoder extends CeaDecoder { }; private final ParsableByteArray ccData; - private final StringBuilder captionStringBuilder; - + private final int packetLength; private final int selectedField; private int captionMode; @@ -179,10 +183,11 @@ public final class Cea608Decoder extends CeaDecoder { private byte repeatableControlCc1; private byte repeatableControlCc2; - public Cea608Decoder(int accessibilityChannel) { + public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - captionStringBuilder = new StringBuilder(); + + packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { case 3: case 4: @@ -238,8 +243,9 @@ public final class Cea608Decoder extends CeaDecoder { ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); boolean captionDataProcessed = false; boolean isRepeatableControl = false; - while (ccData.bytesLeft() > 0) { - byte ccDataHeader = (byte) ccData.readUnsignedByte(); + while (ccData.bytesLeft() >= packetLength) { + byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER + : (byte) ccData.readUnsignedByte(); byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index b690362fb7..aef55892a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -70,7 +70,8 @@ public final class MimeTypes { public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g"; - public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4vtt"; + public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt"; + public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608"; public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc"; public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; From 8dc810405964770fe227a333b1b4d0f3fdfef0a3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Nov 2016 09:33:32 -0800 Subject: [PATCH 151/206] Load the next period only if its start point is known. If a period has no enabled tracks it is considered to be fully buffered, even if its duration is unknown. This would cause the offset of the next loading period to be set based on the unset duration of the preceding period, in turn causing the from of the player to expose a position based on an unset value. Only load the next period when the current one has a known duration. If a period has no enabled tracks and an unknown duration this causes the player to play the period indefinitely. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140613858 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 0e0c30ffc0..1c82130131 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1068,8 +1068,9 @@ import java.io.IOException; } if (loadingPeriodHolder == null - || (loadingPeriodHolder.isFullyBuffered() && !loadingPeriodHolder.isLast - && (playingPeriodHolder == null + || (loadingPeriodHolder.isFullyBuffered() + && timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() != C.TIME_UNSET + && !loadingPeriodHolder.isLast && (playingPeriodHolder == null || loadingPeriodHolder.index - playingPeriodHolder.index < MAXIMUM_BUFFER_AHEAD_PERIODS))) { // We don't have a loading period or it's fully loaded, so try and create the next one. int newLoadingPeriodIndex = loadingPeriodHolder == null ? playbackInfo.periodIndex From 195a93e5af12715c4d92b376fb6f40d8fd8885d8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Nov 2016 10:46:18 -0800 Subject: [PATCH 152/206] Fix playback control focus ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140623107 --- .../exoplayer2/demo/PlayerActivity.java | 11 ++ demo/src/main/res/layout/player_activity.xml | 2 - .../exoplayer2/ui/PlaybackControlView.java | 104 +++++++++++++----- .../exoplayer2/ui/SimpleExoPlayerView.java | 26 ++++- .../res/layout/exo_playback_control_view.xml | 5 +- library/src/main/res/values/styles.xml | 4 +- 6 files changed, 111 insertions(+), 41 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index b3cc8c0ba5..267c54cc05 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -22,6 +22,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; +import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; @@ -194,6 +195,16 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + // Activity input + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Show the controls on any key event. + simpleExoPlayerView.showController(); + // If the event was not handled then see if the player view can handle it as a media key event. + return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event); + } + // OnClickListener methods @Override diff --git a/demo/src/main/res/layout/player_activity.xml b/demo/src/main/res/layout/player_activity.xml index 07ac5e2ba1..3f8cdaa7d6 100644 --- a/demo/src/main/res/layout/player_activity.xml +++ b/demo/src/main/res/layout/player_activity.xml @@ -16,13 +16,11 @@ diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 83f1615310..54812185e7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -236,6 +236,8 @@ public class PlaybackControlView extends FrameLayout { componentListener = new ComponentListener(); LayoutInflater.from(context).inflate(controllerLayoutId, this); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + durationView = (TextView) findViewById(R.id.exo_duration); positionView = (TextView) findViewById(R.id.exo_position); progressBar = (SeekBar) findViewById(R.id.exo_progress); @@ -359,6 +361,7 @@ public class PlaybackControlView extends FrameLayout { visibilityListener.onVisibilityChange(getVisibility()); } updateAll(); + requestPlayPauseFocus(); } // Call hideAfterTimeout even if already visible to reset the timeout. hideAfterTimeout(); @@ -408,12 +411,18 @@ public class PlaybackControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow) { return; } + boolean requestPlayPauseFocus = false; boolean playing = player != null && player.getPlayWhenReady(); if (playButton != null) { - playButton.setVisibility(playing ? GONE : VISIBLE); + requestPlayPauseFocus |= playing && playButton.isFocused(); + playButton.setVisibility(playing ? View.GONE : View.VISIBLE); } if (pauseButton != null) { - pauseButton.setVisibility(playing ? VISIBLE : GONE); + requestPlayPauseFocus |= !playing && pauseButton.isFocused(); + pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); + } + if (requestPlayPauseFocus) { + requestPlayPauseFocus(); } } @@ -481,6 +490,15 @@ public class PlaybackControlView extends FrameLayout { } } + private void requestPlayPauseFocus() { + boolean playing = player != null && player.getPlayWhenReady(); + if (!playing && playButton != null) { + playButton.requestFocus(); + } else if (playing && pauseButton != null) { + pauseButton.requestFocus(); + } + } + private void setButtonEnabled(boolean enabled, View view) { if (view == null) { return; @@ -590,40 +608,66 @@ public class PlaybackControlView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) { - return super.dispatchKeyEvent(event); + boolean handled = dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + if (handled) { + show(); } - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - case KeyEvent.KEYCODE_DPAD_RIGHT: - fastForward(); - break; - case KeyEvent.KEYCODE_MEDIA_REWIND: - case KeyEvent.KEYCODE_DPAD_LEFT: - rewind(); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - player.setPlayWhenReady(!player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - player.setPlayWhenReady(true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - player.setPlayWhenReady(false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - return false; + return handled; + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (player == null || !isHandledMediaKey(keyCode)) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + fastForward(); + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + player.setPlayWhenReady(!player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + player.setPlayWhenReady(true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + player.setPlayWhenReady(false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } } show(); return true; } + private static boolean isHandledMediaKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD + || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } + private final class ComponentListener implements ExoPlayer.EventListener, SeekBar.OnSeekBarChangeListener, OnClickListener { diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 5acb3bfb45..4a138ba232 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -216,6 +216,7 @@ public final class SimpleExoPlayerView extends FrameLayout { LayoutInflater.from(context).inflate(playerLayoutId, this); componentListener = new ComponentListener(); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. contentFrame = (AspectRatioFrameLayout) findViewById(R.id.exo_content_frame); @@ -376,6 +377,26 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. Does nothing if playback controls are disabled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + return useController && controller.dispatchMediaKeyEvent(event); + } + + /** + * Shows the playback controls. Does nothing if playback controls are disabled. + */ + public void showController() { + if (useController) { + maybeShowController(true); + } + } + /** * Returns the playback controls timeout. The playback controls are automatically hidden after * this duration of time has elapsed without user input and with playback or buffering in @@ -473,11 +494,6 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); - } - private void maybeShowController(boolean isForced) { if (!useController || player == null) { return; diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index 1ea6b3a582..f8ef5a6fdd 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -17,9 +17,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" + android:layoutDirection="ltr" android:background="#CC000000" - android:orientation="vertical" - android:layoutDirection="ltr"> + android:orientation="vertical"> @null - 71dip - 52dip + 71dp + 52dp + +
    diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index c3fc16495c..b5c01b4575 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -14,6 +14,7 @@ limitations under the License. --> + @@ -21,6 +22,7 @@ + diff --git a/library/src/main/res/values/constants.xml b/library/src/main/res/values/constants.xml new file mode 100644 index 0000000000..5c86696ea0 --- /dev/null +++ b/library/src/main/res/values/constants.xml @@ -0,0 +1,21 @@ + + + + + 71dp + 52dp + + diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml index f55c8f6945..61db83825e 100644 --- a/library/src/main/res/values/ids.xml +++ b/library/src/main/res/values/ids.xml @@ -14,6 +14,7 @@ limitations under the License. --> + @@ -29,4 +30,5 @@ + diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml index a08bf902fa..a67cffe420 100644 --- a/library/src/main/res/values/styles.xml +++ b/library/src/main/res/values/styles.xml @@ -17,8 +17,8 @@