Commit 8cbcfd66 by ojw28 Committed by GitHub

Merge pull request #4556 from google/dev-v2-r2.8.3

r2.8.3
parents f7ed789f 563f13a8
Showing with 1954 additions and 707 deletions
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
<option name="USE_RELATIVE_INDENTS" value="false" />
</value>
</option>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="JD_P_AT_EMPTY_LINES" value="false" />
<option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
<option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
<option name="JD_KEEP_EMPTY_RETURN" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<Objective-C>
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
<option name="INDENT_CLASS_MEMBERS" value="2" />
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
</Objective-C>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cc" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:.*Style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_weight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_margin</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:padding</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>
# Release notes # # Release notes #
### 2.8.3 ###
* IMA:
* Fix behavior when creating/releasing the player then releasing
`ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
* Add support for setting slots for companion ads.
* Captions:
* TTML: Fix an issue with TTML using font size as % of cell resolution that
makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly.
([#4491](https://github.com/google/ExoPlayer/issues/4491)).
* CEA-608: Improve handling of embedded styles
([#4321](https://github.com/google/ExoPlayer/issues/4321)).
* DASH:
* Exclude text streams from duration calculations
([#4029](https://github.com/google/ExoPlayer/issues/4029)).
* Fix freezing when playing multi-period manifests with `EventStream`s
([#4492](https://github.com/google/ExoPlayer/issues/4492)).
* DRM: Allow DrmInitData to carry a license server URL
([#3393](https://github.com/google/ExoPlayer/issues/3393)).
* MPEG-TS: Fix bug preventing SCTE-35 cues from being output
([#4573](https://github.com/google/ExoPlayer/issues/4573)).
* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using
CommentFrame to InternalFrame for frames with gapless metadata in MP4.
* Add `PlayerView.isControllerVisible`
([#4385](https://github.com/google/ExoPlayer/issues/4385)).
* Fix issue playing DRM protected streams on Asus Zenfone 2
([#4403](https://github.com/google/ExoPlayer/issues/4413)).
* Add support for multiple audio and video tracks in MPEG-PS streams
([#4406](https://github.com/google/ExoPlayer/issues/4406)).
* Add workaround for track index mismatches between trex and tkhd boxes in
fragmented MP4 files
([#4477](https://github.com/google/ExoPlayer/issues/4477)).
* Add workaround for track index mismatches between tfhd and tkhd boxes in
fragmented MP4 files
([#4083](https://github.com/google/ExoPlayer/issues/4083)).
* Ignore all MP4 edit lists if one edit list couldn't be handled
([#4348](https://github.com/google/ExoPlayer/issues/4348)).
* Fix issue when switching track selection from an embedded track to a primary
track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)).
* Fix accessibility class name for `DefaultTimeBar`
([#4611](https://github.com/google/ExoPlayer/issues/4611)).
* Improved compatibility with FireOS devices.
### 2.8.2 ### ### 2.8.2 ###
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an * IMA: Don't advertise support for video/mpeg ad media, as we don't have an
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.8.2' releaseVersion = '2.8.3'
releaseVersionCode = 2802 releaseVersionCode = 2803
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -282,7 +283,7 @@ import java.util.ArrayList; ...@@ -282,7 +283,7 @@ import java.util.ArrayList;
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex(); updateCurrentItemIndex();
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
castMediaQueueCreationPending = true; castMediaQueueCreationPending = true;
......
...@@ -38,6 +38,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager; ...@@ -38,6 +38,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
...@@ -62,6 +63,7 @@ import java.lang.annotation.Retention; ...@@ -62,6 +63,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The expected ad group index that IMA should load next. */ /** The expected ad group index that IMA should load next. */
private int expectedAdGroupIndex; private int expectedAdGroupIndex;
/** /** The index of the current ad group that IMA is loading. */
* The index of the current ad group that IMA is loading.
*/
private int adGroupIndex; private int adGroupIndex;
/** /** Whether IMA has sent an ad event to pause content since the last resume content event. */
* Whether IMA has sent an ad event to pause content since the last resume content event.
*/
private boolean imaPausedContent; private boolean imaPausedContent;
/** The current ad playback state. */ /** The current ad playback state. */
private @ImaAdState int imaAdState; private @ImaAdState int imaAdState;
...@@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state. // Fields tracking the player/loader state.
/** /** Whether the player is playing an ad. */
* Whether the player is playing an ad.
*/
private boolean playingAd; private boolean playingAd;
/** /**
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
...@@ -310,13 +306,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -310,13 +306,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* content progress should increase. {@link C#TIME_UNSET} otherwise. * content progress should increase. {@link C#TIME_UNSET} otherwise.
*/ */
private long fakeContentProgressOffsetMs; private long fakeContentProgressOffsetMs;
/** /** Stores the pending content position when a seek operation was intercepted to play an ad. */
* Stores the pending content position when a seek operation was intercepted to play an ad.
*/
private long pendingContentPositionMs; private long pendingContentPositionMs;
/** /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs; private boolean sentPendingContentPositionMs;
/** /**
...@@ -406,6 +398,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -406,6 +398,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
/** /**
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
* ImaSdkFactory#createCompanionAdSlot()}.
*
* @param companionSlots Slots for displaying companion ads.
* @see AdDisplayContainer#setCompanionSlots(Collection)
*/
public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {
adDisplayContainer.setCompanionSlots(companionSlots);
}
/**
* Requests ads, if they have not already been requested. Must be called on the main thread. * Requests ads, if they have not already been requested. Must be called on the main thread.
* *
* <p>Ads will be requested automatically when the player is prepared if this method has not been * <p>Ads will be requested automatically when the player is prepared if this method has not been
...@@ -509,6 +512,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -509,6 +512,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.destroy(); adsManager.destroy();
adsManager = null; adsManager = null;
} }
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE;
updateAdPlaybackState();
} }
@Override @Override
...@@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdEvent: " + adEventType); Log.d(TAG, "onAdEvent: " + adEventType);
} }
if (adsManager == null) { if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent); Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
return; return;
} }
try { try {
...@@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void loadAd(String adUriString) { public void loadAd(String adUriString) {
try { try {
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
if (adsManager == null) {
Log.w(TAG, "Ignoring loadAd after release");
return;
}
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
Log.w( Log.w(
TAG, TAG,
...@@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adGroupIndex = expectedAdGroupIndex; adGroupIndex = expectedAdGroupIndex;
adsManager.start(); adsManager.start();
} }
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) { if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
...@@ -693,6 +705,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -693,6 +705,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd"); Log.d(TAG, "playAd");
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring playAd after release");
return;
}
switch (imaAdState) { switch (imaAdState) {
case IMA_AD_STATE_PLAYING: case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content. // IMA does not always call stopAd before resuming content.
...@@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "stopAd"); Log.d(TAG, "stopAd");
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring stopAd after release");
return;
}
if (player == null) { if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected stopAd while detached"); Log.w(TAG, "Unexpected stopAd while detached");
...@@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released. // The player is being reset and this source will be released.
return; return;
...@@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d( Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring ad prepare error after release");
return;
}
if (imaAdState == IMA_AD_STATE_NONE) { if (imaAdState == IMA_AD_STATE_NONE) {
// Send IMA a content position at the ad group so that it will try to play it, at which point // Send IMA a content position at the ad group so that it will try to play it, at which point
// we can notify that it failed to load. // we can notify that it failed to load.
...@@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.e(TAG, message, cause); Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads. // We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) { if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState(); adPlaybackState = AdPlaybackState.NONE;
} else { } else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) { for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
......
...@@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback(); Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
......
...@@ -674,8 +674,8 @@ public final class MediaSessionConnector { ...@@ -674,8 +674,8 @@ public final class MediaSessionConnector {
private int currentWindowCount; private int currentWindowCount;
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount(); int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) { if (queueNavigator != null) {
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.8.2"; public static final String VERSION = "2.8.3";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.2"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.3";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2008002; public static final int VERSION_INT = 2008003;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -80,6 +80,13 @@ public final class Format implements Parcelable { ...@@ -80,6 +80,13 @@ public final class Format implements Parcelable {
/** DRM initialization data if the stream is protected, or null otherwise. */ /** DRM initialization data if the stream is protected, or null otherwise. */
public final @Nullable DrmInitData drmInitData; public final @Nullable DrmInitData drmInitData;
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
* timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
* relative to the timestamps of their parent samples.
*/
public final long subsampleOffsetUs;
// Video specific. // Video specific.
/** /**
...@@ -141,15 +148,6 @@ public final class Format implements Parcelable { ...@@ -141,15 +148,6 @@ public final class Format implements Parcelable {
*/ */
public final int encoderPadding; public final int encoderPadding;
// Text specific.
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
* timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
* relative to the timestamps of their parent samples.
*/
public final long subsampleOffsetUs;
// Audio and text specific. // Audio and text specific.
/** /**
......
...@@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions;
reading = playing.next; reading = playing.next;
} }
playing.release(); playing.release();
playing = playing.next;
length--; length--;
if (length == 0) { if (length == 0) {
loading = null; loading = null;
oldFrontPeriodUid = playing.uid;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
} }
playing = playing.next;
} else { } else {
playing = loading; playing = loading;
reading = loading; reading = loading;
......
...@@ -191,7 +191,8 @@ public interface Player { ...@@ -191,7 +191,8 @@ public interface Player {
* @param manifest The latest manifest. May be null. * @param manifest The latest manifest. May be null.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/ */
void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason);
/** /**
* Called when the available or selected tracks change. * Called when the available or selected tracks change.
...@@ -281,8 +282,8 @@ public interface Player { ...@@ -281,8 +282,8 @@ public interface Player {
abstract class DefaultEventListener implements EventListener { abstract class DefaultEventListener implements EventListener {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
// Call deprecated version. Otherwise, do nothing. // Call deprecated version. Otherwise, do nothing.
onTimelineChanged(timeline, manifest); onTimelineChanged(timeline, manifest);
} }
...@@ -337,7 +338,7 @@ public interface Player { ...@@ -337,7 +338,7 @@ public interface Player {
* instead. * instead.
*/ */
@Deprecated @Deprecated
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing. // Do nothing.
} }
......
...@@ -420,7 +420,7 @@ public class AnalyticsCollector ...@@ -420,7 +420,7 @@ public class AnalyticsCollector
@Override @Override
public final void onTimelineChanged( public final void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline); mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
......
...@@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor { ...@@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
* that part of audio as silent, in microseconds. * that part of audio as silent, in microseconds.
*/ */
private static final long MINIMUM_SILENCE_DURATION_US = 100_000; private static final long MINIMUM_SILENCE_DURATION_US = 150_000;
/** /**
* The duration of silence by which to extend non-silent sections, in microseconds. The value must * The duration of silence by which to extend non-silent sections, in microseconds. The value must
* not exceed {@link #MINIMUM_SILENCE_DURATION_US}. * not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
*/ */
private static final long PADDING_SILENCE_US = 10_000; private static final long PADDING_SILENCE_US = 20_000;
/** /**
* The absolute level below which an individual PCM sample is classified as silent. Note: the * The absolute level below which an individual PCM sample is classified as silent. Note: the
* specified value will be rounded so that the threshold check only depends on the more * specified value will be rounded so that the threshold check only depends on the more
......
...@@ -22,11 +22,12 @@ import android.os.Handler; ...@@ -22,11 +22,12 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher;
import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import java.util.Arrays; import java.util.Arrays;
...@@ -77,8 +78,7 @@ import java.util.UUID; ...@@ -77,8 +78,7 @@ import java.util.UUID;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
private final ProvisioningManager<T> provisioningManager; private final ProvisioningManager<T> provisioningManager;
private final byte[] initData; private final SchemeData schemeData;
private final String mimeType;
private final @DefaultDrmSessionManager.Mode int mode; private final @DefaultDrmSessionManager.Mode int mode;
private final HashMap<String, String> optionalKeyRequestParameters; private final HashMap<String, String> optionalKeyRequestParameters;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
...@@ -97,15 +97,20 @@ import java.util.UUID; ...@@ -97,15 +97,20 @@ import java.util.UUID;
private byte[] sessionId; private byte[] sessionId;
private byte[] offlineLicenseKeySetId; private byte[] offlineLicenseKeySetId;
private Object currentKeyRequest;
private Object currentProvisionRequest;
/** /**
* Instantiates a new DRM session. * Instantiates a new DRM session.
* *
* @param uuid The UUID of the drm scheme. * @param uuid The UUID of the drm scheme.
* @param mediaDrm The media DRM. * @param mediaDrm The media DRM.
* @param provisioningManager The manager for provisioning. * @param provisioningManager The manager for provisioning.
* @param initData The DRM init data. * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is
* provided.
* @param mode The DRM mode. * @param mode The DRM mode.
* @param offlineLicenseKeySetId The offlineLicense KeySetId. * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using
* offline keys.
* @param optionalKeyRequestParameters The optional key request parameters. * @param optionalKeyRequestParameters The optional key request parameters.
* @param callback The media DRM callback. * @param callback The media DRM callback.
* @param playbackLooper The playback looper. * @param playbackLooper The playback looper.
...@@ -117,10 +122,9 @@ import java.util.UUID; ...@@ -117,10 +122,9 @@ import java.util.UUID;
UUID uuid, UUID uuid,
ExoMediaDrm<T> mediaDrm, ExoMediaDrm<T> mediaDrm,
ProvisioningManager<T> provisioningManager, ProvisioningManager<T> provisioningManager,
byte[] initData, @Nullable SchemeData schemeData,
String mimeType,
@DefaultDrmSessionManager.Mode int mode, @DefaultDrmSessionManager.Mode int mode,
byte[] offlineLicenseKeySetId, @Nullable byte[] offlineLicenseKeySetId,
HashMap<String, String> optionalKeyRequestParameters, HashMap<String, String> optionalKeyRequestParameters,
MediaDrmCallback callback, MediaDrmCallback callback,
Looper playbackLooper, Looper playbackLooper,
...@@ -131,6 +135,7 @@ import java.util.UUID; ...@@ -131,6 +135,7 @@ import java.util.UUID;
this.mediaDrm = mediaDrm; this.mediaDrm = mediaDrm;
this.mode = mode; this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId; this.offlineLicenseKeySetId = offlineLicenseKeySetId;
this.schemeData = offlineLicenseKeySetId == null ? schemeData : null;
this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.callback = callback; this.callback = callback;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
...@@ -141,14 +146,6 @@ import java.util.UUID; ...@@ -141,14 +146,6 @@ import java.util.UUID;
requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread = new HandlerThread("DrmRequestHandler");
requestHandlerThread.start(); requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
if (offlineLicenseKeySetId == null) {
this.initData = initData;
this.mimeType = mimeType;
} else {
this.initData = null;
this.mimeType = null;
}
} }
// Life cycle. // Life cycle.
...@@ -177,6 +174,8 @@ import java.util.UUID; ...@@ -177,6 +174,8 @@ import java.util.UUID;
requestHandlerThread = null; requestHandlerThread = null;
mediaCrypto = null; mediaCrypto = null;
lastException = null; lastException = null;
currentKeyRequest = null;
currentProvisionRequest = null;
if (sessionId != null) { if (sessionId != null) {
mediaDrm.closeSession(sessionId); mediaDrm.closeSession(sessionId);
sessionId = null; sessionId = null;
...@@ -187,18 +186,42 @@ import java.util.UUID; ...@@ -187,18 +186,42 @@ import java.util.UUID;
} }
public boolean hasInitData(byte[] initData) { public boolean hasInitData(byte[] initData) {
return Arrays.equals(this.initData, initData); return Arrays.equals(schemeData != null ? schemeData.data : null, initData);
} }
public boolean hasSessionId(byte[] sessionId) { public boolean hasSessionId(byte[] sessionId) {
return Arrays.equals(this.sessionId, sessionId); return Arrays.equals(this.sessionId, sessionId);
} }
@SuppressWarnings("deprecation")
public void onMediaDrmEvent(int what) {
if (!isOpen()) {
return;
}
switch (what) {
case ExoMediaDrm.EVENT_KEY_REQUIRED:
doLicense(false);
break;
case ExoMediaDrm.EVENT_KEY_EXPIRED:
// When an already expired key is loaded MediaDrm sends this event immediately. Ignore
// this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
// waiting for key response.
onKeysExpired();
break;
case ExoMediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED;
provisioningManager.provisionRequired(this);
break;
default:
break;
}
}
// Provisioning implementation. // Provisioning implementation.
public void provision() { public void provision() {
ProvisionRequest request = mediaDrm.getProvisionRequest(); currentProvisionRequest = mediaDrm.getProvisionRequest();
postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget(); postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true);
} }
public void onProvisionCompleted() { public void onProvisionCompleted() {
...@@ -271,11 +294,12 @@ import java.util.UUID; ...@@ -271,11 +294,12 @@ import java.util.UUID;
return false; return false;
} }
private void onProvisionResponse(Object response) { private void onProvisionResponse(Object request, Object response) {
if (state != STATE_OPENING && !isOpen()) { if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) {
// This event is stale. // This event is stale.
return; return;
} }
currentProvisionRequest = null;
if (response instanceof Exception) { if (response instanceof Exception) {
provisioningManager.onProvisionError((Exception) response); provisioningManager.onProvisionError((Exception) response);
...@@ -356,24 +380,30 @@ import java.util.UUID; ...@@ -356,24 +380,30 @@ import java.util.UUID;
private void postKeyRequest(int type, boolean allowRetry) { private void postKeyRequest(int type, boolean allowRetry) {
byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId;
byte[] initData = null;
String mimeType = null;
String licenseServerUrl = null;
if (schemeData != null) {
initData = schemeData.data;
mimeType = schemeData.mimeType;
licenseServerUrl = schemeData.licenseServerUrl;
}
try { try {
KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, KeyRequest mediaDrmKeyRequest =
optionalKeyRequestParameters); mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters);
if (C.CLEARKEY_UUID.equals(uuid)) { currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl);
request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry);
request.getDefaultUrl());
}
postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
} catch (Exception e) { } catch (Exception e) {
onKeysError(e); onKeysError(e);
} }
} }
private void onKeyResponse(Object response) { private void onKeyResponse(Object request, Object response) {
if (!isOpen()) { if (request != currentKeyRequest || !isOpen()) {
// This event is stale. // This event is stale.
return; return;
} }
currentKeyRequest = null;
if (response instanceof Exception) { if (response instanceof Exception) {
onKeysError((Exception) response); onKeysError((Exception) response);
...@@ -382,9 +412,6 @@ import java.util.UUID; ...@@ -382,9 +412,6 @@ import java.util.UUID;
try { try {
byte[] responseData = (byte[]) response; byte[] responseData = (byte[]) response;
if (C.CLEARKEY_UUID.equals(uuid)) {
responseData = ClearKeyUtil.adjustResponseData(responseData);
}
if (mode == DefaultDrmSessionManager.MODE_RELEASE) { if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
eventDispatcher.drmKeysRemoved(); eventDispatcher.drmKeysRemoved();
...@@ -430,30 +457,7 @@ import java.util.UUID; ...@@ -430,30 +457,7 @@ import java.util.UUID;
return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
} }
@SuppressWarnings("deprecation") // Internal classes.
public void onMediaDrmEvent(int what) {
if (!isOpen()) {
return;
}
switch (what) {
case ExoMediaDrm.EVENT_KEY_REQUIRED:
doLicense(false);
break;
case ExoMediaDrm.EVENT_KEY_EXPIRED:
// When an already expired key is loaded MediaDrm sends this event immediately. Ignore
// this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
// waiting for key response.
onKeysExpired();
break;
case ExoMediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED;
provisioningManager.provisionRequired(this);
break;
default:
break;
}
}
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler { private class PostResponseHandler extends Handler {
...@@ -464,12 +468,15 @@ import java.util.UUID; ...@@ -464,12 +468,15 @@ import java.util.UUID;
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
Pair<?, ?> requestAndResponse = (Pair<?, ?>) msg.obj;
Object request = requestAndResponse.first;
Object response = requestAndResponse.second;
switch (msg.what) { switch (msg.what) {
case MSG_PROVISION: case MSG_PROVISION:
onProvisionResponse(msg.obj); onProvisionResponse(request, response);
break; break;
case MSG_KEYS: case MSG_KEYS:
onKeyResponse(msg.obj); onKeyResponse(request, response);
break; break;
default: default:
break; break;
...@@ -486,21 +493,27 @@ import java.util.UUID; ...@@ -486,21 +493,27 @@ import java.util.UUID;
super(backgroundLooper); super(backgroundLooper);
} }
Message obtainMessage(int what, Object object, boolean allowRetry) { void post(int what, Object request, boolean allowRetry) {
return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */, int allowRetryInt = allowRetry ? 1 : 0;
object); int errorCount = 0;
obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget();
} }
@Override @Override
@SuppressWarnings("unchecked")
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
Object request = msg.obj;
Object response; Object response;
try { try {
switch (msg.what) { switch (msg.what) {
case MSG_PROVISION: case MSG_PROVISION:
response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request);
break; break;
case MSG_KEYS: case MSG_KEYS:
response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); Pair<KeyRequest, String> keyRequest = (Pair<KeyRequest, String>) request;
KeyRequest mediaDrmKeyRequest = keyRequest.first;
String licenseServerUrl = keyRequest.second;
response = callback.executeKeyRequest(uuid, mediaDrmKeyRequest, licenseServerUrl);
break; break;
default: default:
throw new RuntimeException(); throw new RuntimeException();
...@@ -511,7 +524,7 @@ import java.util.UUID; ...@@ -511,7 +524,7 @@ import java.util.UUID;
} }
response = e; response = e;
} }
postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget();
} }
private boolean maybeRetryRequest(Message originalMsg) { private boolean maybeRetryRequest(Message originalMsg) {
...@@ -534,5 +547,4 @@ import java.util.UUID; ...@@ -534,5 +547,4 @@ import java.util.UUID;
} }
} }
} }
...@@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; ...@@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
private static final String TAG = "DefaultDrmSessionMgr"; private static final String TAG = "DefaultDrmSessionMgr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid; private final UUID uuid;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
...@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} }
} }
byte[] initData = null; SchemeData schemeData = null;
String mimeType = null;
if (offlineLicenseKeySetId == null) { if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid, false); schemeData = getSchemeData(drmInitData, uuid, false);
if (data == null) { if (schemeData == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid); final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
eventDispatcher.drmSessionManagerError(error); eventDispatcher.drmSessionManagerError(error);
return new ErrorStateDrmSession<>(new DrmSessionException(error)); return new ErrorStateDrmSession<>(new DrmSessionException(error));
} }
initData = getSchemeInitData(data, uuid);
mimeType = getSchemeMimeType(data, uuid);
} }
DefaultDrmSession<T> session; DefaultDrmSession<T> session;
...@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} else { } else {
// Only use an existing session if it has matching init data. // Only use an existing session if it has matching init data.
session = null; session = null;
byte[] initData = schemeData != null ? schemeData.data : null;
for (DefaultDrmSession<T> existingSession : sessions) { for (DefaultDrmSession<T> existingSession : sessions) {
if (existingSession.hasInitData(initData)) { if (existingSession.hasInitData(initData)) {
session = existingSession; session = existingSession;
...@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
uuid, uuid,
mediaDrm, mediaDrm,
this, this,
initData, schemeData,
mimeType,
mode, mode,
offlineLicenseKeySetId, offlineLicenseKeySetId,
optionalKeyRequestParameters, optionalKeyRequestParameters,
...@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
return matchingSchemeDatas.get(0); return matchingSchemeDatas.get(0);
} }
private static byte[] getSchemeInitData(SchemeData data, UUID uuid) {
byte[] schemeInitData = data.data;
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitData = psshData;
}
}
return schemeInitData;
}
private static String getSchemeMimeType(SchemeData data, UUID uuid) {
String schemeMimeType = data.mimeType;
if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
|| MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
schemeMimeType = CENC_SCHEME_MIME_TYPE;
}
return schemeMimeType;
}
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
private class MediaDrmHandler extends Handler { private class MediaDrmHandler extends Handler {
......
...@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* applies to all schemes). * applies to all schemes).
*/ */
private final UUID uuid; private final UUID uuid;
/** /** The URL of the server to which license requests should be made. May be null if unknown. */
* The mimeType of {@link #data}. public final @Nullable String licenseServerUrl;
*/ /** The mimeType of {@link #data}. */
public final String mimeType; public final String mimeType;
/** /**
* The initialization data. May be null for scheme support checks only. * The initialization data. May be null for scheme support checks only.
...@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param requiresSecureDecryption See {@link #requiresSecureDecryption}. * @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
*/ */
public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption);
}
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
* @param licenseServerUrl See {@link #licenseServerUrl}.
* @param mimeType See {@link #mimeType}.
* @param data See {@link #data}.
* @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
*/
public SchemeData(
UUID uuid,
@Nullable String licenseServerUrl,
String mimeType,
byte[] data,
boolean requiresSecureDecryption) {
this.uuid = Assertions.checkNotNull(uuid); this.uuid = Assertions.checkNotNull(uuid);
this.licenseServerUrl = licenseServerUrl;
this.mimeType = Assertions.checkNotNull(mimeType); this.mimeType = Assertions.checkNotNull(mimeType);
this.data = data; this.data = data;
this.requiresSecureDecryption = requiresSecureDecryption; this.requiresSecureDecryption = requiresSecureDecryption;
...@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
/* package */ SchemeData(Parcel in) { /* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong()); uuid = new UUID(in.readLong(), in.readLong());
licenseServerUrl = in.readString();
mimeType = in.readString(); mimeType = in.readString();
data = in.createByteArray(); data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0; requiresSecureDecryption = in.readByte() != 0;
...@@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return true; return true;
} }
SchemeData other = (SchemeData) obj; SchemeData other = (SchemeData) obj;
return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data); && Arrays.equals(data, other.data);
} }
...@@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {
int result = uuid.hashCode(); int result = uuid.hashCode();
result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
result = 31 * result + mimeType.hashCode(); result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data); result = 31 * result + Arrays.hashCode(data);
hashCode = result; hashCode = result;
...@@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits());
dest.writeString(licenseServerUrl);
dest.writeString(mimeType); dest.writeString(mimeType);
dest.writeByteArray(data); dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.DeniedByServerException; import android.media.DeniedByServerException;
import android.media.MediaCrypto; import android.media.MediaCrypto;
...@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException; ...@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
...@@ -40,6 +43,8 @@ import java.util.UUID; ...@@ -40,6 +43,8 @@ import java.util.UUID;
@TargetApi(23) @TargetApi(23)
public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> { public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid; private final UUID uuid;
private final MediaDrm mediaDrm; private final MediaDrm mediaDrm;
...@@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
} }
} }
@SuppressLint("WrongConstant")
private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException { private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
Assertions.checkNotNull(uuid); Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
...@@ -67,6 +73,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -67,6 +73,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
uuid = Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid; uuid = Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;
this.uuid = uuid; this.uuid = uuid;
this.mediaDrm = new MediaDrm(uuid); this.mediaDrm = new MediaDrm(uuid);
if (C.WIDEVINE_UUID.equals(uuid) && needsForceL3Workaround()) {
mediaDrm.setPropertyString("securityLevel", "L3");
}
} }
@Override @Override
...@@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override @Override
public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType, public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
HashMap<String, String> optionalParameters) throws NotProvisionedException { HashMap<String, String> optionalParameters) throws NotProvisionedException {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon
// devices also required data to be extracted from the PSSH atom for PlayReady.
if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid))
|| (C.PLAYREADY_UUID.equals(uuid)
&& "Amazon".equals(Util.MANUFACTURER)
&& ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
|| "AFTS".equals(Util.MODEL) // Fire TV Gen 2
|| "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid);
if (psshData == null) {
// Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged.
} else {
init = psshData;
}
}
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
if (Util.SDK_INT < 26
&& C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {
mimeType = CENC_SCHEME_MIME_TYPE;
}
final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType,
optionalParameters); optionalParameters);
return new DefaultKeyRequest(request.getData(), request.getDefaultUrl());
byte[] requestData = request.getData();
if (C.CLEARKEY_UUID.equals(uuid)) {
requestData = ClearKeyUtil.adjustRequestData(requestData);
}
return new DefaultKeyRequest(requestData, request.getDefaultUrl());
} }
@Override @Override
public byte[] provideKeyResponse(byte[] scope, byte[] response) public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException { throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response);
}
return mediaDrm.provideKeyResponse(scope, response); return mediaDrm.provideKeyResponse(scope, response);
} }
...@@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
forceAllowInsecureDecoderComponents); forceAllowInsecureDecoderComponents);
} }
/**
* Returns whether the device codec is known to fail if security level L1 is used.
*
* <p>See <a href="https://github.com/google/ExoPlayer/issues/4413">GitHub issue #4413</a>.
*/
private static boolean needsForceL3Workaround() {
return "ASUS_Z00AD".equals(Util.MODEL);
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
...@@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
} }
@Override @Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
String url = request.getDefaultUrl(); String url = request.getDefaultUrl();
if (TextUtils.isEmpty(url)) {
url = mediaProvidedLicenseServerUrl;
}
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl; url = defaultLicenseUrl;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback { ...@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback {
} }
@Override @Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
return keyResponse; return keyResponse;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import java.util.UUID; import java.util.UUID;
...@@ -38,10 +39,13 @@ public interface MediaDrmCallback { ...@@ -38,10 +39,13 @@ public interface MediaDrmCallback {
* Executes a key request. * Executes a key request.
* *
* @param uuid The UUID of the content protection scheme. * @param uuid The UUID of the content protection scheme.
* @param request The request. * @param request The request generated by the content decryption module.
* @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the
* media does not include any license server URL.
* @return The response data. * @return The response data.
* @throws Exception If an error occurred executing the request. * @throws Exception If an error occurred executing the request.
*/ */
byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception;
} }
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -39,7 +40,8 @@ public final class GaplessInfoHolder { ...@@ -39,7 +40,8 @@ public final class GaplessInfoHolder {
} }
}; };
private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final String GAPLESS_DOMAIN = "com.apple.iTunes";
private static final String GAPLESS_DESCRIPTION = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN = private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
...@@ -91,7 +93,15 @@ public final class GaplessInfoHolder { ...@@ -91,7 +93,15 @@ public final class GaplessInfoHolder {
Metadata.Entry entry = metadata.get(i); Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) { if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry; CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) { if (GAPLESS_DESCRIPTION.equals(commentFrame.description)
&& setFromComment(commentFrame.text)) {
return true;
}
} else if (entry instanceof InternalFrame) {
InternalFrame internalFrame = (InternalFrame) entry;
if (GAPLESS_DOMAIN.equals(internalFrame.domain)
&& GAPLESS_DESCRIPTION.equals(internalFrame.description)
&& setFromComment(internalFrame.text)) {
return true; return true;
} }
} }
...@@ -103,14 +113,10 @@ public final class GaplessInfoHolder { ...@@ -103,14 +113,10 @@ public final class GaplessInfoHolder {
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * 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. * or MPEG 4 user data), if valid and non-zero.
* *
* @param name The comment's identifier.
* @param data The comment's payload data. * @param data The comment's payload data.
* @return Whether the holder was populated. * @return Whether the holder was populated.
*/ */
private boolean setFromComment(String name, String data) { private boolean setFromComment(String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) { if (matcher.find()) {
try { try {
......
...@@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor { ...@@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.number = (int) value; currentTrack.number = (int) value;
break; break;
case ID_FLAG_DEFAULT: case ID_FLAG_DEFAULT:
currentTrack.flagForced = value == 1; currentTrack.flagDefault = value == 1;
break; break;
case ID_FLAG_FORCED: case ID_FLAG_FORCED:
currentTrack.flagDefault = value == 1; currentTrack.flagForced = value == 1;
break; break;
case ID_TRACK_TYPE: case ID_TRACK_TYPE:
currentTrack.type = (int) value; currentTrack.type = (int) value;
......
...@@ -43,6 +43,9 @@ import java.util.List; ...@@ -43,6 +43,9 @@ import java.util.List;
*/ */
/* package */ final class AtomParsers { /* package */ final class AtomParsers {
/** Thrown if an edit list couldn't be applied. */
public static final class UnhandledEditListException extends ParserException {}
private static final String TAG = "AtomParsers"; private static final String TAG = "AtomParsers";
private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
...@@ -117,10 +120,12 @@ import java.util.List; ...@@ -117,10 +120,12 @@ import java.util.List;
* @param stblAtom stbl (sample table) atom to decode. * @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information. * @param gaplessInfoHolder Holder to populate with gapless playback information.
* @return Sample table described by the stbl atom. * @return Sample table described by the stbl atom.
* @throws ParserException If the resulting sample sequence does not contain a sync sample. * @throws UnhandledEditListException Thrown if the edit list can't be applied.
* @throws ParserException Thrown if the stbl atom can't be parsed.
*/ */
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, public static TrackSampleTable parseStbl(
GaplessInfoHolder gaplessInfoHolder) throws ParserException { Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
throws ParserException {
SampleSizeBox sampleSizeBox; SampleSizeBox sampleSizeBox;
Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) { if (stszAtom != null) {
...@@ -136,7 +141,13 @@ import java.util.List; ...@@ -136,7 +141,13 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount(); int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) { if (sampleCount == 0) {
return new TrackSampleTable( return new TrackSampleTable(
new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); track,
/* offsets= */ new long[0],
/* sizes= */ new int[0],
/* maximumSize= */ 0,
/* timestampsUs= */ new long[0],
/* flags= */ new int[0],
/* durationUs= */ C.TIME_UNSET);
} }
// Entries are byte offsets of chunks. // Entries are byte offsets of chunks.
...@@ -315,7 +326,8 @@ import java.util.List; ...@@ -315,7 +326,8 @@ import java.util.List;
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // 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. // This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
...@@ -342,7 +354,8 @@ import java.util.List; ...@@ -342,7 +354,8 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding; gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
} }
} }
...@@ -359,7 +372,8 @@ import java.util.List; ...@@ -359,7 +372,8 @@ import java.util.List;
} }
durationUs = durationUs =
Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// Omit any sample at the end point of an edit for audio tracks. // Omit any sample at the end point of an edit for audio tracks.
...@@ -409,6 +423,11 @@ import java.util.List; ...@@ -409,6 +423,11 @@ import java.util.List;
System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
} }
if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// Applying the edit list would require prerolling from a sync sample.
Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample.");
throw new UnhandledEditListException();
}
for (int j = startIndex; j < endIndex; j++) { for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs = long timeInSegmentUs =
...@@ -424,20 +443,8 @@ import java.util.List; ...@@ -424,20 +443,8 @@ import java.util.List;
pts += editDuration; pts += editDuration;
} }
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale); long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
boolean hasSyncSample = false;
for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
}
if (!hasSyncSample) {
// We don't support edit lists where the edited sample sequence doesn't contain a sync sample.
// Such edit lists are often (although not always) broken, so we ignore it and continue.
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
return new TrackSampleTable( return new TrackSampleTable(
track,
editedOffsets, editedOffsets,
editedSizes, editedSizes,
editedMaximumSize, editedMaximumSize,
......
...@@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i); Track track = tracks.valueAt(i);
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
trackBundle.init(track, defaultSampleValuesArray.get(track.id)); trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
trackBundles.put(track.id, trackBundle); trackBundles.put(track.id, trackBundle);
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
} }
...@@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor {
Assertions.checkState(trackBundles.size() == trackCount); Assertions.checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i); Track track = tracks.valueAt(i);
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); trackBundles
.get(track.id)
.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
} }
} }
} }
private DefaultSampleValues getDefaultSampleValues(
SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) {
if (defaultSampleValuesArray.size() == 1) {
// Ignore track id if there is only one track to cope with non-matching track indices.
// See https://github.com/google/ExoPlayer/issues/4477.
return defaultSampleValuesArray.valueAt(/* index= */ 0);
}
return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));
}
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
parseMoof(moof, trackBundles, flags, extendedTypeScratch); parseMoof(moof, trackBundles, flags, extendedTypeScratch);
// If drm init data is sideloaded, we ignore pssh boxes. // If drm init data is sideloaded, we ignore pssh boxes.
...@@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray, private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException { @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) { if (trackBundle == null) {
return; return;
} }
...@@ -793,13 +805,13 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -793,13 +805,13 @@ public final class FragmentedMp4Extractor implements Extractor {
* @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd
* does not refer to any {@link TrackBundle}. * does not refer to any {@link TrackBundle}.
*/ */
private static TrackBundle parseTfhd(ParsableByteArray tfhd, private static TrackBundle parseTfhd(
SparseArray<TrackBundle> trackBundles, int flags) { ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) {
tfhd.setPosition(Atom.HEADER_SIZE); tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt(); int fullAtom = tfhd.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom); int atomFlags = Atom.parseFullAtomFlags(fullAtom);
int trackId = tfhd.readInt(); int trackId = tfhd.readInt();
TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
if (trackBundle == null) { if (trackBundle == null) {
return null; return null;
} }
...@@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor {
return trackBundle; return trackBundle;
} }
private static @Nullable TrackBundle getTrackBundle(
SparseArray<TrackBundle> trackBundles, int trackId) {
if (trackBundles.size() == 1) {
// Ignore track id if there is only one track. This is either because we have a side-loaded
// track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see
// https://github.com/google/ExoPlayer/issues/4083).
return trackBundles.valueAt(/* index= */ 0);
}
return trackBundles.get(trackId);
}
/** /**
* Parses a tfdt atom (defined in 14496-12). * Parses a tfdt atom (defined in 14496-12).
* *
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util; ...@@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(atomSize - 12); data.skipBytes(atomSize - 12);
} }
} }
if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { if (domain == null || name == null || dataAtomPosition == -1) {
// We're only interested in iTunSMPB.
return null; return null;
} }
data.setPosition(dataAtomPosition); data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(dataAtomSize - 16); String value = data.readNullTerminatedString(dataAtomSize - 16);
return new CommentFrame(LANGUAGE_UNDEFINED, name, value); return new InternalFrame(domain, name, value);
} }
private static int parseUint8AttributeValue(ParsableByteArray data) { private static int parseUint8AttributeValue(ParsableByteArray data) {
......
...@@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} }
} }
for (int i = 0; i < moov.containerChildren.size(); i++) { boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
Atom.ContainerAtom atom = moov.containerChildren.get(i); ArrayList<TrackSampleTable> trackSampleTables;
if (atom.type != Atom.TYPE_trak) { try {
continue; trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
} } catch (AtomParsers.UnhandledEditListException e) {
// Discard gapless info as we aren't able to handle corresponding edits.
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), gaplessInfoHolder = new GaplessInfoHolder();
C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); trackSampleTables =
if (track == null) { getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true);
continue; }
}
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
if (trackSampleTable.sampleCount == 0) {
continue;
}
int trackCount = trackSampleTables.size();
for (int i = 0; i < trackCount; i++) {
TrackSampleTable trackSampleTable = trackSampleTables.get(i);
Track track = trackSampleTable.track;
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
extractorOutput.track(i, track.type)); extractorOutput.track(i, track.type));
// Each sample has up to three bytes of overhead for the start code that replaces its length. // Each sample has up to three bytes of overhead for the start code that replaces its length.
...@@ -445,6 +441,39 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -445,6 +441,39 @@ public final class Mp4Extractor implements Extractor, SeekMap {
extractorOutput.seekMap(this); extractorOutput.seekMap(this);
} }
private ArrayList<TrackSampleTable> getTrackSampleTables(
ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)
throws ParserException {
ArrayList<TrackSampleTable> trackSampleTables = new ArrayList<>();
for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) {
continue;
}
Track track =
AtomParsers.parseTrak(
atom,
moov.getLeafAtomOfType(Atom.TYPE_mvhd),
/* duration= */ C.TIME_UNSET,
/* drmInitData= */ null,
ignoreEditLists,
isQuickTime);
if (track == null) {
continue;
}
Atom.ContainerAtom stblAtom =
atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf)
.getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
if (trackSampleTable.sampleCount == 0) {
continue;
}
trackSampleTables.add(trackSampleTable);
}
return trackSampleTables;
}
/** /**
* Attempts to extract the next sample in the current mdat atom for the specified track. * Attempts to extract the next sample in the current mdat atom for the specified track.
* <p> * <p>
......
...@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/ */
/* package */ final class TrackSampleTable { /* package */ final class TrackSampleTable {
/** /** The track corresponding to this sample table. */
* Number of samples. public final Track track;
*/ /** Number of samples. */
public final int sampleCount; public final int sampleCount;
/** /** Sample offsets in bytes. */
* Sample offsets in bytes.
*/
public final long[] offsets; public final long[] offsets;
/** /** Sample sizes in bytes. */
* Sample sizes in bytes.
*/
public final int[] sizes; public final int[] sizes;
/** /** Maximum sample size in {@link #sizes}. */
* Maximum sample size in {@link #sizes}.
*/
public final int maximumSize; public final int maximumSize;
/** /** Sample timestamps in microseconds. */
* Sample timestamps in microseconds.
*/
public final long[] timestampsUs; public final long[] timestampsUs;
/** /** Sample flags. */
* Sample flags.
*/
public final int[] flags; public final int[] flags;
/** /**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
...@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
public final long durationUs; public final long durationUs;
public TrackSampleTable( public TrackSampleTable(
Track track,
long[] offsets, long[] offsets,
int[] sizes, int[] sizes,
int maximumSize, int maximumSize,
...@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length);
this.track = track;
this.offsets = offsets; this.offsets = offsets;
this.sizes = sizes; this.sizes = sizes;
this.maximumSize = maximumSize; this.maximumSize = maximumSize;
......
...@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor { ...@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor {
private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
// Max search length for first audio and video track in input data.
private static final long MAX_SEARCH_LENGTH = 1024 * 1024; private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
// Max search length for additional audio and video tracks in input data after at least one audio
// and video track has been found.
private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD; public static final int PRIVATE_STREAM_1 = 0xBD;
public static final int AUDIO_STREAM = 0xC0; public static final int AUDIO_STREAM = 0xC0;
...@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor { ...@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor {
private boolean foundAllTracks; private boolean foundAllTracks;
private boolean foundAudioTrack; private boolean foundAudioTrack;
private boolean foundVideoTrack; private boolean foundVideoTrack;
private long lastTrackPosition;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private ExtractorOutput output; private ExtractorOutput output;
...@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor { ...@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) { if (!foundAllTracks) {
if (payloadReader == null) { if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null; ElementaryStreamReader elementaryStreamReader = null;
if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio. // Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only // NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs. // valid for DVDs.
elementaryStreamReader = new Ac3Reader(); elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { lastTrackPosition = input.getPosition();
} else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader(); elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { lastTrackPosition = input.getPosition();
} else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader(); elementaryStreamReader = new H262Reader();
foundVideoTrack = true; foundVideoTrack = true;
lastTrackPosition = input.getPosition();
} }
if (elementaryStreamReader != null) { if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
...@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor { ...@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor {
psPayloadReaders.put(streamId, payloadReader); psPayloadReaders.put(streamId, payloadReader);
} }
} }
if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { long maxSearchPosition =
foundAudioTrack && foundVideoTrack
? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
: MAX_SEARCH_LENGTH;
if (input.getPosition() > maxSearchPosition) {
foundAllTracks = true; foundAllTracks = true;
output.endTracks(); output.endTracks();
} }
......
...@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
} }
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys.
return;
}
}
} }
if (codecInfo == null) { if (codecInfo == null) {
...@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
...@@ -1210,6 +1219,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1210,6 +1219,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
/** /**
* Returns whether the device needs keys to have been loaded into the {@link DrmSession} before
* codec configuration.
*/
private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {
return "Amazon".equals(Util.MANUFACTURER)
&& ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
|| "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
}
/**
* Returns whether the decoder is known to fail when flushed. * Returns whether the decoder is known to fail when flushed.
* <p> * <p>
* If true is returned, the renderer will work around the issue by releasing the decoder and * If true is returned, the renderer will work around the issue by releasing the decoder and
...@@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
/** /**
* Returns whether the decoder is known to handle the propagation of the * Returns whether the decoder is known to handle the propagation of the {@link
* {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
* <p> *
* If true is returned, the renderer will work around the issue by approximating end of stream * <p>If true is returned, the renderer will work around the issue by approximating end of stream
* behavior without relying on the flag being propagated through to an output buffer by the * behavior without relying on the flag being propagated through to an output buffer by the
* underlying decoder. * underlying decoder.
* *
* @param name The name of the decoder. * @param codecInfo Information about the {@link MediaCodec}.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise. * propagation incorrectly on the host device. False otherwise.
*/ */
private static boolean codecNeedsEosPropagationWorkaround(String name) { private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) String name = codecInfo.name;
|| "OMX.allwinner.video.decoder.avc".equals(name)); return (Util.SDK_INT <= 17
&& ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
} }
/** /**
......
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/** Internal ID3 frame that is intended for use by the player. */
public final class InternalFrame extends Id3Frame {
public static final String ID = "----";
public final String domain;
public final String description;
public final String text;
public InternalFrame(String domain, String description, String text) {
super(ID);
this.domain = domain;
this.description = description;
this.text = text;
}
/* package */ InternalFrame(Parcel in) {
super(ID);
domain = Assertions.checkNotNull(in.readString());
description = Assertions.checkNotNull(in.readString());
text = Assertions.checkNotNull(in.readString());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
InternalFrame other = (InternalFrame) obj;
return Util.areEqual(description, other.description)
&& Util.areEqual(domain, other.domain)
&& Util.areEqual(text, other.text);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (domain != null ? domain.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
return result;
}
@Override
public String toString() {
return id + ": domain=" + domain + ", description=" + description;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(domain);
dest.writeString(text);
}
public static final Creator<InternalFrame> CREATOR =
new Creator<InternalFrame>() {
@Override
public InternalFrame createFromParcel(Parcel in) {
return new InternalFrame(in);
}
@Override
public InternalFrame[] newArray(int size) {
return new InternalFrame[size];
}
};
}
...@@ -86,6 +86,7 @@ public abstract class DownloadService extends Service { ...@@ -86,6 +86,7 @@ public abstract class DownloadService extends Service {
private DownloadManagerListener downloadManagerListener; private DownloadManagerListener downloadManagerListener;
private int lastStartId; private int lastStartId;
private boolean startedInForeground; private boolean startedInForeground;
private boolean taskRemoved;
/** /**
* Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
...@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service { ...@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId; lastStartId = startId;
taskRemoved = false;
String intentAction = null; String intentAction = null;
if (intent != null) { if (intent != null) {
intentAction = intent.getAction(); intentAction = intent.getAction();
startedInForeground |= startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
} }
// intentAction is null if the service is restarted or no action is specified.
if (intentAction == null) {
intentAction = ACTION_INIT;
}
logd("onStartCommand action: " + intentAction + " startId: " + startId); logd("onStartCommand action: " + intentAction + " startId: " + startId);
switch (intentAction) { switch (intentAction) {
case ACTION_INIT: case ACTION_INIT:
...@@ -261,6 +267,12 @@ public abstract class DownloadService extends Service { ...@@ -261,6 +267,12 @@ public abstract class DownloadService extends Service {
} }
@Override @Override
public void onTaskRemoved(Intent rootIntent) {
logd("onTaskRemoved rootIntent: " + rootIntent);
taskRemoved = true;
}
@Override
public void onDestroy() { public void onDestroy() {
logd("onDestroy"); logd("onDestroy");
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
...@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service { ...@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service {
if (startedInForeground && Util.SDK_INT >= 26) { if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready(); foregroundNotificationUpdater.showNotificationIfNotAlready();
} }
boolean stopSelfResult = stopSelfResult(lastStartId); if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); stopSelf();
logd("stopSelf()");
} else {
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
} }
private void logd(String message) { private void logd(String message) {
......
...@@ -344,6 +344,14 @@ public final class AdPlaybackState { ...@@ -344,6 +344,14 @@ public final class AdPlaybackState {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** Returns an instance with the specified ad marked as skipped. */
@CheckResult
public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as having a load error. */ /** Returns an instance with the specified ad marked as having a load error. */
@CheckResult @CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
......
...@@ -21,10 +21,10 @@ import android.text.Layout.Alignment; ...@@ -21,10 +21,10 @@ import android.text.Layout.Alignment;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
...@@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder {
private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
private static final int[] COLORS = new int[] {
Color.WHITE, private static final int[] STYLE_COLORS =
Color.GREEN, new int[] {
Color.BLUE, Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA
Color.CYAN, };
Color.RED, private static final int STYLE_ITALICS = 0x07;
Color.YELLOW, private static final int STYLE_UNCHANGED = 0x08;
Color.MAGENTA,
};
// The default number of rows to display in roll-up captions mode. // The default number of rows to display in roll-up captions mode.
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
...@@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder {
// A midrow control code advances the cursor. // A midrow control code advances the cursor.
currentCueBuilder.append(' '); currentCueBuilder.append(' ');
// cc2 - 0|0|1|0|ATRBT|U // cc2 - 0|0|1|0|STYLE|U
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle boolean underline = (cc2 & 0x01) == 0x01;
boolean isUnderlined = (cc2 & 0x01) == 0x01; int style = (cc2 >> 1) & 0x07;
currentCueBuilder.setUnderline(isUnderlined); currentCueBuilder.setStyle(style, underline);
int attribute = (cc2 >> 1) & 0x0F;
if (attribute == 0x07) {
currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
} else {
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
}
} }
private void handlePreambleAddressCode(byte cc1, byte cc2) { private void handlePreambleAddressCode(byte cc1, byte cc2) {
...@@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder {
currentCueBuilder.setRow(row); currentCueBuilder.setRow(row);
} }
if ((cc2 & 0x01) == 0x01) {
currentCueBuilder.setPreambleStyle(new UnderlineSpan());
}
// cc2 - 0|1|N|0|STYLE|U // cc2 - 0|1|N|0|STYLE|U
// cc2 - 0|1|N|1|CURSR|U // cc2 - 0|1|N|1|CURSR|U
int attribute = cc2 >> 1 & 0x0F; boolean isCursor = (cc2 & 0x10) == 0x10;
if (attribute <= 0x07) { boolean underline = (cc2 & 0x01) == 0x01;
if (attribute == 0x07) { int cursorOrStyle = (cc2 >> 1) & 0x07;
currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); // We need to call setStyle even for the isCursor case, to update the underline bit.
} else { // STYLE_UNCHANGED is used for this case.
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);
}
} else { if (isCursor) {
currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]);
} }
} }
...@@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder {
private static class CueBuilder { private static class CueBuilder {
private static final int POSITION_UNSET = -1;
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
// positions to normalized screen position. // positions to normalized screen position.
private static final int SCREEN_CHARWIDTH = 32; private static final int SCREEN_CHARWIDTH = 32;
private static final int BASE_ROW = 15; private static final int BASE_ROW = 15;
private final List<CharacterStyle> preambleStyles; private final List<CueStyle> cueStyles;
private final List<CueStyle> midrowStyles;
private final List<SpannableString> rolledUpCaptions; private final List<SpannableString> rolledUpCaptions;
private final SpannableStringBuilder captionStringBuilder; private final StringBuilder captionStringBuilder;
private int row; private int row;
private int indent; private int indent;
private int tabOffset; private int tabOffset;
private int captionMode; private int captionMode;
private int captionRowCount; private int captionRowCount;
private int underlineStartPosition;
public CueBuilder(int captionMode, int captionRowCount) { public CueBuilder(int captionMode, int captionRowCount) {
preambleStyles = new ArrayList<>(); cueStyles = new ArrayList<>();
midrowStyles = new ArrayList<>();
rolledUpCaptions = new ArrayList<>(); rolledUpCaptions = new ArrayList<>();
captionStringBuilder = new SpannableStringBuilder(); captionStringBuilder = new StringBuilder();
reset(captionMode); reset(captionMode);
setCaptionRowCount(captionRowCount); setCaptionRowCount(captionRowCount);
} }
public void reset(int captionMode) { public void reset(int captionMode) {
this.captionMode = captionMode; this.captionMode = captionMode;
preambleStyles.clear(); cueStyles.clear();
midrowStyles.clear();
rolledUpCaptions.clear(); rolledUpCaptions.clear();
captionStringBuilder.clear(); captionStringBuilder.setLength(0);
row = BASE_ROW; row = BASE_ROW;
indent = 0; indent = 0;
tabOffset = 0; tabOffset = 0;
underlineStartPosition = POSITION_UNSET;
} }
public void setCaptionRowCount(int captionRowCount) { public void setCaptionRowCount(int captionRowCount) {
...@@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder {
} }
public boolean isEmpty() { public boolean isEmpty() {
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() return cueStyles.isEmpty()
&& rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0; && captionStringBuilder.length() == 0;
} }
...@@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder {
int length = captionStringBuilder.length(); int length = captionStringBuilder.length();
if (length > 0) { if (length > 0) {
captionStringBuilder.delete(length - 1, length); captionStringBuilder.delete(length - 1, length);
// Decrement style start positions if necessary.
for (int i = cueStyles.size() - 1; i >= 0; i--) {
CueStyle style = cueStyles.get(i);
if (style.start == length) {
style.start--;
} else {
// All earlier cues must have style.start < length.
break;
}
}
} }
} }
...@@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder {
public void rollUp() { public void rollUp() {
rolledUpCaptions.add(buildSpannableString()); rolledUpCaptions.add(buildSpannableString());
captionStringBuilder.clear(); captionStringBuilder.setLength(0);
preambleStyles.clear(); cueStyles.clear();
midrowStyles.clear();
underlineStartPosition = POSITION_UNSET;
int numRows = Math.min(captionRowCount, row); int numRows = Math.min(captionRowCount, row);
while (rolledUpCaptions.size() >= numRows) { while (rolledUpCaptions.size() >= numRows) {
rolledUpCaptions.remove(0); rolledUpCaptions.remove(0);
...@@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder {
tabOffset = tabs; tabOffset = tabs;
} }
public void setPreambleStyle(CharacterStyle style) { public void setStyle(int style, boolean underline) {
preambleStyles.add(style); cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));
}
public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
}
public void setUnderline(boolean enabled) {
if (enabled) {
underlineStartPosition = captionStringBuilder.length();
} else if (underlineStartPosition != POSITION_UNSET) {
// underline spans won't overlap, so it's safe to modify the builder directly with them
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
underlineStartPosition = POSITION_UNSET;
}
} }
public void append(char text) { public void append(char text) {
...@@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder {
} }
public SpannableString buildSpannableString() { public SpannableString buildSpannableString() {
int length = captionStringBuilder.length(); SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);
int length = builder.length();
int underlineStartPosition = C.INDEX_UNSET;
int italicStartPosition = C.INDEX_UNSET;
int colorStartPosition = 0;
int color = Color.WHITE;
boolean nextItalic = false;
int nextColor = Color.WHITE;
for (int i = 0; i < cueStyles.size(); i++) {
CueStyle cueStyle = cueStyles.get(i);
boolean underline = cueStyle.underline;
int style = cueStyle.style;
if (style != STYLE_UNCHANGED) {
// If the style is a color then italic is cleared.
nextItalic = style == STYLE_ITALICS;
// If the style is italic then the color is left unchanged.
nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];
}
// preamble styles apply to the entire cue int position = cueStyle.start;
for (int i = 0; i < preambleStyles.size(); i++) { int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;
captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, if (position == nextPosition) {
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // There are more cueStyles to process at the current position.
} continue;
}
// midrow styles only apply to part of the cue, and after preamble styles // Process changes to underline up to the current position.
for (int i = 0; i < midrowStyles.size(); i++) { if (underlineStartPosition != C.INDEX_UNSET && !underline) {
CueStyle cueStyle = midrowStyles.get(i); setUnderlineSpan(builder, underlineStartPosition, position);
int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) underlineStartPosition = C.INDEX_UNSET;
? midrowStyles.get(i + cueStyle.nextStyleIncrement).start } else if (underlineStartPosition == C.INDEX_UNSET && underline) {
: length; underlineStartPosition = position;
captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, }
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // Process changes to italic up to the current position.
if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {
setItalicSpan(builder, italicStartPosition, position);
italicStartPosition = C.INDEX_UNSET;
} else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {
italicStartPosition = position;
}
// Process changes to color up to the current position.
if (nextColor != color) {
setColorSpan(builder, colorStartPosition, position, color);
color = nextColor;
colorStartPosition = position;
}
} }
// special case for midrow underlines that went to the end of the cue // Add any final spans.
if (underlineStartPosition != POSITION_UNSET) { if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, setUnderlineSpan(builder, underlineStartPosition, length);
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }
if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {
setItalicSpan(builder, italicStartPosition, length);
}
if (colorStartPosition != length) {
setColorSpan(builder, colorStartPosition, length, color);
} }
return new SpannableString(captionStringBuilder); return new SpannableString(builder);
} }
public Cue build() { public Cue build() {
...@@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder {
return captionStringBuilder.toString(); return captionStringBuilder.toString();
} }
private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {
builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static void setColorSpan(
SpannableStringBuilder builder, int start, int end, int color) {
if (color == Color.WHITE) {
// White is treated as the default color (i.e. no span is attached).
return;
}
builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static class CueStyle { private static class CueStyle {
public final CharacterStyle style; public final int style;
public final int start; public final boolean underline;
public final int nextStyleIncrement;
public int start;
public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { public CueStyle(int style, boolean underline, int start) {
this.style = style; this.style = style;
this.underline = underline;
this.start = start; this.start = start;
this.nextStyleIncrement = nextStyleIncrement;
} }
} }
......
...@@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference; ...@@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class DefaultTrackSelector extends MappingTrackSelector { public class DefaultTrackSelector extends MappingTrackSelector {
/** /**
* A builder for {@link Parameters}. * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of
* the parameters that can be configured using this builder.
*/ */
public static final class ParametersBuilder { public static final class ParametersBuilder {
...@@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private boolean viewportOrientationMayChange; private boolean viewportOrientationMayChange;
private int tunnelingAudioSessionId; private int tunnelingAudioSessionId;
/** /** Creates a builder with default initial values. */
* Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}.
*/
public ParametersBuilder() { public ParametersBuilder() {
this(Parameters.DEFAULT); this(Parameters.DEFAULT);
} }
...@@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
/** /**
* Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
* {@link Util#getPhysicalDisplaySize(Context)}. * obtained from {@link Util#getPhysicalDisplaySize(Context)}.
* *
* @param context The context to obtain the viewport size from. * @param context Any context.
* @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder. * @return This builder.
*/ */
public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, public ParametersBuilder setViewportSizeToPhysicalDisplaySize(
boolean viewportOrientationMayChange) { Context context, boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen. // Assume the viewport is fullscreen.
Point viewportSize = Util.getPhysicalDisplaySize(context); Point viewportSize = Util.getPhysicalDisplaySize(context);
return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
...@@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
/** /**
* See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link
* {@link Parameters#viewportOrientationMayChange}. * Parameters#viewportOrientationMayChange}.
* *
* @param viewportWidth See {@link Parameters#viewportWidth}.
* @param viewportHeight See {@link Parameters#viewportHeight}.
* @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder. * @return This builder.
*/ */
public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, public ParametersBuilder setViewportSize(
boolean viewportOrientationMayChange) { int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.viewportWidth = viewportWidth; this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight; this.viewportHeight = viewportHeight;
this.viewportOrientationMayChange = viewportOrientationMayChange; this.viewportOrientationMayChange = viewportOrientationMayChange;
...@@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
/** /**
* Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in * See {@link Parameters#tunnelingAudioSessionId}.
* tunneling mode. Session ids can be generated using {@link *
* <p>Enables or disables tunneling. To enable tunneling, pass an audio session id to use when
* in tunneling mode. Session ids can be generated using {@link
* C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link
* C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
* supported by the audio and video renderers for the selected tracks. * supported by the audio and video renderers for the selected tracks.
...@@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** Constraint parameters for {@link DefaultTrackSelector}. */ /** Constraint parameters for {@link DefaultTrackSelector}. */
public static final class Parameters implements Parcelable { public static final class Parameters implements Parcelable {
/** /** An instance with default values. */
* An instance with default values:
*
* <ul>
* <li>No preferred audio language.
* <li>No preferred text language.
* <li>Text tracks with undetermined language are not selected if no track with {@link
* #preferredTextLanguage} is available.
* <li>All selection flags are considered for text track selections.
* <li>Lowest bitrate track selections are not forced.
* <li>Adaptation between different mime types is not allowed.
* <li>Non seamless adaptation is allowed.
* <li>No max limit for video width/height.
* <li>No max video bitrate.
* <li>Video constraints are exceeded if no supported selection can be made otherwise.
* <li>Renderer capabilities are exceeded if no supported selection can be made.
* <li>No viewport constraints.
* </ul>
*/
public static final Parameters DEFAULT = new Parameters(); public static final Parameters DEFAULT = new Parameters();
// Per renderer overrides. // Per renderer overrides.
...@@ -568,105 +554,131 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -568,105 +554,131 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Audio // Audio
/** /**
* The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null}
* {@code null} selects the default track, or the first track if there's no default. * selects the default track, or the first track if there's no default. The default value is
* {@code null}.
*/ */
public final @Nullable String preferredAudioLanguage; public final @Nullable String preferredAudioLanguage;
// Text // Text
/** /**
* The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the
* default track if there is one, or no track otherwise. * default track if there is one, or no track otherwise. The default value is {@code null}.
*/ */
public final @Nullable String preferredTextLanguage; public final @Nullable String preferredTextLanguage;
/** /**
* Whether a text track with undetermined language should be selected if no track with * Whether a text track with undetermined language should be selected if no track with {@link
* {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The
* default value is {@code false}.
*/ */
public final boolean selectUndeterminedTextLanguage; public final boolean selectUndeterminedTextLanguage;
/** /**
* Bitmask of selection flags that are disabled for text track selections. See {@link * Bitmask of selection flags that are disabled for text track selections. See {@link
* C.SelectionFlags}. * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).
*/ */
public final int disabledTextTrackSelectionFlags; public final int disabledTextTrackSelectionFlags;
// Video // Video
/** /**
* Maximum allowed video width. * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no
* constraint).
*
* <p>To constrain adaptive video track selections to be suitable for a given viewport (the
* region of the display within which video will be played), use ({@link #viewportWidth}, {@link
* #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
*/ */
public final int maxVideoWidth; public final int maxVideoWidth;
/** /**
* Maximum allowed video height. * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no
* constraint).
*
* <p>To constrain adaptive video track selections to be suitable for a given viewport (the
* region of the display within which video will be played), use ({@link #viewportWidth}, {@link
* #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
*/ */
public final int maxVideoHeight; public final int maxVideoHeight;
/** /**
* Maximum video bitrate. * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint).
*/ */
public final int maxVideoBitrate; public final int maxVideoBitrate;
/** /**
* Whether to exceed video constraints when no selection can be made otherwise. * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link
* #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is
* {@code true}.
*/ */
public final boolean exceedVideoConstraintsIfNecessary; public final boolean exceedVideoConstraintsIfNecessary;
/** /**
* Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that * Viewport width in pixels. Constrains video track selections for adaptive content so that only
* only tracks suitable for the viewport are selected. * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE}
* (i.e. no constraint).
*/ */
public final int viewportWidth; public final int viewportWidth;
/** /**
* Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that * Viewport height in pixels. Constrains video track selections for adaptive content so that
* only tracks suitable for the viewport are selected. * only tracks suitable for the viewport are selected. The default value is {@link
* Integer#MAX_VALUE} (i.e. no constraint).
*/ */
public final int viewportHeight; public final int viewportHeight;
/** /**
* Whether the viewport orientation may change during playback. Constrains video tracks * Whether the viewport orientation may change during playback. Constrains video track
* selections for adaptive playbacks so that only tracks suitable for the viewport are selected. * selections for adaptive content so that only tracks suitable for the viewport are selected.
* The default value is {@code true}.
*/ */
public final boolean viewportOrientationMayChange; public final boolean viewportOrientationMayChange;
// General // General
/** /**
* Whether to force selection of the single lowest bitrate audio and video tracks that comply * Whether to force selection of the single lowest bitrate audio and video tracks that comply
* with all other constraints. * with all other constraints. The default value is {@code false}.
*/ */
public final boolean forceLowestBitrate; public final boolean forceLowestBitrate;
/** /**
* Whether to allow adaptive selections containing mixed mime types. * Whether to allow adaptive selections containing mixed mime types. The default value is {@code
* false}.
*/ */
public final boolean allowMixedMimeAdaptiveness; public final boolean allowMixedMimeAdaptiveness;
/** /**
* Whether to allow adaptive selections where adaptation may not be completely seamless. * Whether to allow adaptive selections where adaptation may not be completely seamless. The
* default value is {@code true}.
*/ */
public final boolean allowNonSeamlessAdaptiveness; public final boolean allowNonSeamlessAdaptiveness;
/** /**
* Whether to exceed renderer capabilities when no selection can be made otherwise. * Whether to exceed renderer capabilities when no selection can be made otherwise.
*
* <p>This parameter applies when all of the tracks available for a renderer exceed the
* renderer's reported capabilities. If the parameter is {@code true} then the lowest quality
* track will still be selected. Playback may succeed if the renderer has under-reported its
* true capabilities. If {@code false} then no track will be selected. The default value is
* {@code true}.
*/ */
public final boolean exceedRendererCapabilitiesIfNecessary; public final boolean exceedRendererCapabilitiesIfNecessary;
/** /**
* The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
* is not to be enabled. * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is
* disabled).
*/ */
public final int tunnelingAudioSessionId; public final int tunnelingAudioSessionId;
private Parameters() { private Parameters() {
this( this(
new SparseArray<Map<TrackGroupArray, SelectionOverride>>(), /* selectionOverrides= */ new SparseArray<Map<TrackGroupArray,SelectionOverride>>(),
new SparseBooleanArray(), /* rendererDisabledFlags= */ new SparseBooleanArray(),
null, /* preferredAudioLanguage= */ null,
null, /* preferredTextLanguage= */ null,
false, /* selectUndeterminedTextLanguage= */ false,
0, /* disabledTextTrackSelectionFlags= */ 0,
false, /* forceLowestBitrate= */ false,
false, /* allowMixedMimeAdaptiveness= */ false,
true, /* allowNonSeamlessAdaptiveness= */ true,
Integer.MAX_VALUE, /* maxVideoWidth= */ Integer.MAX_VALUE,
Integer.MAX_VALUE, /* maxVideoHeight= */ Integer.MAX_VALUE,
Integer.MAX_VALUE, /* maxVideoBitrate= */ Integer.MAX_VALUE,
true, /* exceedVideoConstraintsIfNecessary= */ true,
true, /* exceedRendererCapabilitiesIfNecessary= */ true,
Integer.MAX_VALUE, /* viewportWidth= */ Integer.MAX_VALUE,
Integer.MAX_VALUE, /* viewportHeight= */ Integer.MAX_VALUE,
true, /* viewportOrientationMayChange= */ true,
C.AUDIO_SESSION_ID_UNSET); /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET);
} }
/* package */ Parameters( /* package */ Parameters(
......
...@@ -30,7 +30,8 @@ public final class TimestampAdjuster { ...@@ -30,7 +30,8 @@ public final class TimestampAdjuster {
public static final long DO_NOT_OFFSET = Long.MAX_VALUE; public static final long DO_NOT_OFFSET = Long.MAX_VALUE;
/** /**
* The value one greater than the largest representable (33 bit) MPEG-2 TS presentation timestamp. * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock
* presentation timestamp.
*/ */
private static final long MAX_PTS_PLUS_ONE = 0x200000000L; private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
...@@ -38,13 +39,13 @@ public final class TimestampAdjuster { ...@@ -38,13 +39,13 @@ public final class TimestampAdjuster {
private long timestampOffsetUs; private long timestampOffsetUs;
// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
private volatile long lastSampleTimestamp; private volatile long lastSampleTimestampUs;
/** /**
* @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
*/ */
public TimestampAdjuster(long firstSampleTimestampUs) { public TimestampAdjuster(long firstSampleTimestampUs) {
lastSampleTimestamp = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs); setFirstSampleTimestampUs(firstSampleTimestampUs);
} }
...@@ -56,30 +57,24 @@ public final class TimestampAdjuster { ...@@ -56,30 +57,24 @@ public final class TimestampAdjuster {
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
*/ */
public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
this.firstSampleTimestampUs = firstSampleTimestampUs; this.firstSampleTimestampUs = firstSampleTimestampUs;
} }
/** /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
* Returns the first adjusted sample timestamp in microseconds.
*
* @return The first adjusted sample timestamp in microseconds.
*/
public long getFirstSampleTimestampUs() { public long getFirstSampleTimestampUs() {
return firstSampleTimestampUs; return firstSampleTimestampUs;
} }
/** /**
* Returns the last adjusted timestamp. If no timestamp has been adjusted, returns * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link
* {@code firstSampleTimestampUs} as provided to the constructor. If this value is * #adjustSampleTimestamp} has not been called, returns the result of calling {@link
* {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}. * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* * C#TIME_UNSET}.
* @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is
* returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is
* returned.
*/ */
public long getLastAdjustedTimestampUs() { public long getLastAdjustedTimestampUs() {
return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
} }
...@@ -93,44 +88,47 @@ public final class TimestampAdjuster { ...@@ -93,44 +88,47 @@ public final class TimestampAdjuster {
* be offset. * be offset.
*/ */
public long getTimestampOffsetUs() { public long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET ? 0 return firstSampleTimestampUs == DO_NOT_OFFSET
: lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; ? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
} }
/** /**
* Resets the instance to its initial state. * Resets the instance to its initial state.
*/ */
public void reset() { public void reset() {
lastSampleTimestamp = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
} }
/** /**
* Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
* *
* @param pts The MPEG-2 TS presentation timestamp. * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustTsTimestamp(long pts) { public long adjustTsTimestamp(long pts90Khz) {
if (pts == C.TIME_UNSET) { if (pts90Khz == C.TIME_UNSET) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
if (lastSampleTimestamp != C.TIME_UNSET) { if (lastSampleTimestampUs != C.TIME_UNSET) {
// The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
// and we need to snap to the one closest to lastSampleTimestamp. // and we need to snap to the one closest to lastSampleTimestampUs.
long lastPts = usToPts(lastSampleTimestamp); long lastPts = usToPts(lastSampleTimestampUs);
long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE; long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount); long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) pts90Khz =
? ptsWrapBelow : ptsWrapAbove; Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow
: ptsWrapAbove;
} }
return adjustSampleTimestamp(ptsToUs(pts)); return adjustSampleTimestamp(ptsToUs(pts90Khz));
} }
/** /**
* Offsets a sample timestamp in microseconds. * Offsets a timestamp in microseconds.
* *
* @param timeUs The timestamp of a sample to adjust. * @param timeUs The timestamp to adjust in microseconds.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustSampleTimestamp(long timeUs) { public long adjustSampleTimestamp(long timeUs) {
...@@ -138,15 +136,15 @@ public final class TimestampAdjuster { ...@@ -138,15 +136,15 @@ public final class TimestampAdjuster {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
// Record the adjusted PTS to adjust for wraparound next time. // Record the adjusted PTS to adjust for wraparound next time.
if (lastSampleTimestamp != C.TIME_UNSET) { if (lastSampleTimestampUs != C.TIME_UNSET) {
lastSampleTimestamp = timeUs; lastSampleTimestampUs = timeUs;
} else { } else {
if (firstSampleTimestampUs != DO_NOT_OFFSET) { if (firstSampleTimestampUs != DO_NOT_OFFSET) {
// Calculate the timestamp offset. // Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs; timestampOffsetUs = firstSampleTimestampUs - timeUs;
} }
synchronized (this) { synchronized (this) {
lastSampleTimestamp = timeUs; lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized. // Notify threads waiting for this adjuster to be initialized.
notifyAll(); notifyAll();
} }
...@@ -160,15 +158,15 @@ public final class TimestampAdjuster { ...@@ -160,15 +158,15 @@ public final class TimestampAdjuster {
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
public synchronized void waitUntilInitialized() throws InterruptedException { public synchronized void waitUntilInitialized() throws InterruptedException {
while (lastSampleTimestamp == C.TIME_UNSET) { while (lastSampleTimestampUs == C.TIME_UNSET) {
wait(); wait();
} }
} }
/** /**
* Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds. * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
* *
* @param pts A value in MPEG-2 timestamp units. * @param pts A 90 kHz clock timestamp.
* @return The corresponding value in microseconds. * @return The corresponding value in microseconds.
*/ */
public static long ptsToUs(long pts) { public static long ptsToUs(long pts) {
...@@ -176,10 +174,10 @@ public final class TimestampAdjuster { ...@@ -176,10 +174,10 @@ public final class TimestampAdjuster {
} }
/** /**
* Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units. * Converts a timestamp in microseconds to a 90 kHz clock timestamp.
* *
* @param us A value in microseconds. * @param us A value in microseconds.
* @return The corresponding value in MPEG-2 timestamp units. * @return The corresponding value as a 90 kHz clock timestamp.
*/ */
public static long usToPts(long us) { public static long usToPts(long us) {
return (us * 90000) / C.MICROS_PER_SECOND; return (us * 90000) / C.MICROS_PER_SECOND;
......
...@@ -56,8 +56,7 @@ public final class XmlPullParserUtil { ...@@ -56,8 +56,7 @@ public final class XmlPullParserUtil {
* @return Whether the current event is a start tag with the specified name. * @return Whether the current event is a start tag with the specified name.
* @throws XmlPullParserException If an error occurs querying the parser. * @throws XmlPullParserException If an error occurs querying the parser.
*/ */
public static boolean isStartTag(XmlPullParser xpp, String name) public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
throws XmlPullParserException {
return isStartTag(xpp) && xpp.getName().equals(name); return isStartTag(xpp) && xpp.getName().equals(name);
} }
...@@ -73,21 +72,58 @@ public final class XmlPullParserUtil { ...@@ -73,21 +72,58 @@ public final class XmlPullParserUtil {
} }
/** /**
* Returns whether the current event is a start tag with the specified name. If the current event
* has a raw name then its prefix is stripped before matching.
*
* @param xpp The {@link XmlPullParser} to query.
* @param name The specified name.
* @return Whether the current event is a start tag with the specified name.
* @throws XmlPullParserException If an error occurs querying the parser.
*/
public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name)
throws XmlPullParserException {
return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name);
}
/**
* Returns the value of an attribute of the current start tag. * Returns the value of an attribute of the current start tag.
* *
* @param xpp The {@link XmlPullParser} to query. * @param xpp The {@link XmlPullParser} to query.
* @param attributeName The name of the attribute. * @param attributeName The name of the attribute.
* @return The value of the attribute, or null if the current event is not a start tag or if no * @return The value of the attribute, or null if the current event is not a start tag or if no
* no such attribute was found. * such attribute was found.
*/ */
public static String getAttributeValue(XmlPullParser xpp, String attributeName) { public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount(); int attributeCount = xpp.getAttributeCount();
for (int i = 0; i < attributeCount; i++) { for (int i = 0; i < attributeCount; i++) {
if (attributeName.equals(xpp.getAttributeName(i))) { if (xpp.getAttributeName(i).equals(attributeName)) {
return xpp.getAttributeValue(i);
}
}
return null;
}
/**
* Returns the value of an attribute of the current start tag. Any raw attribute names in the
* current start tag have their prefixes stripped before matching.
*
* @param xpp The {@link XmlPullParser} to query.
* @param attributeName The name of the attribute.
* @return The value of the attribute, or null if the current event is not a start tag or if no
* such attribute was found.
*/
public static String getAttributeValueIgnorePrefix(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) {
return xpp.getAttributeValue(i); return xpp.getAttributeValue(i);
} }
} }
return null; return null;
} }
private static String stripPrefix(String name) {
int prefixSeparatorIndex = name.indexOf(':');
return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1);
}
} }
...@@ -84,6 +84,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -84,6 +84,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// pending output streams that have fewer frames than the codec latency. // pending output streams that have fewer frames than the codec latency.
private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10;
private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround;
private static boolean deviceNeedsSetOutputSurfaceWorkaround;
private final Context context; private final Context context;
private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
...@@ -459,7 +462,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -459,7 +462,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width && newFormat.width <= codecMaxValues.width
&& newFormat.height <= codecMaxValues.height && newFormat.height <= codecMaxValues.height
&& getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) {
return oldFormat.initializationDataEquals(newFormat) return oldFormat.initializationDataEquals(newFormat)
? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION
: KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION;
...@@ -981,7 +984,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -981,7 +984,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
throws DecoderQueryException { throws DecoderQueryException {
int maxWidth = format.width; int maxWidth = format.width;
int maxHeight = format.height; int maxHeight = format.height;
int maxInputSize = getMaxInputSize(format); int maxInputSize = getMaxInputSize(codecInfo, format);
if (streamFormats.length == 1) { if (streamFormats.length == 1) {
// The single entry in streamFormats must correspond to the format for which the codec is // The single entry in streamFormats must correspond to the format for which the codec is
// being configured. // being configured.
...@@ -994,7 +997,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -994,7 +997,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
(streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE);
maxWidth = Math.max(maxWidth, streamFormat.width); maxWidth = Math.max(maxWidth, streamFormat.width);
maxHeight = Math.max(maxHeight, streamFormat.height); maxHeight = Math.max(maxHeight, streamFormat.height);
maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat));
} }
} }
if (haveUnknownDimensions) { if (haveUnknownDimensions) {
...@@ -1004,7 +1007,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1004,7 +1007,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maxWidth = Math.max(maxWidth, codecMaxSize.x); maxWidth = Math.max(maxWidth, codecMaxSize.x);
maxHeight = Math.max(maxHeight, codecMaxSize.y); maxHeight = Math.max(maxHeight, codecMaxSize.y);
maxInputSize = maxInputSize =
Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); Math.max(
maxInputSize,
getMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight));
Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight);
} }
} }
...@@ -1053,13 +1058,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1053,13 +1058,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
/** /**
* Returns a maximum input buffer size for a given format. * Returns a maximum input buffer size for a given codec and format.
* *
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The format. * @param format The format.
* @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not
* be determined. * be determined.
*/ */
private static int getMaxInputSize(Format format) { private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) {
if (format.maxInputSize != Format.NO_VALUE) { if (format.maxInputSize != Format.NO_VALUE) {
// The format defines an explicit maximum input size. Add the total size of initialization // The format defines an explicit maximum input size. Add the total size of initialization
// data buffers, as they may need to be queued in the same input buffer as the largest sample. // data buffers, as they may need to be queued in the same input buffer as the largest sample.
...@@ -1072,20 +1078,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1072,20 +1078,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} else { } else {
// Calculated maximum input sizes are overestimates, so it's not necessary to add the size of // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of
// initialization data. // initialization data.
return getMaxInputSize(format.sampleMimeType, format.width, format.height); return getMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height);
} }
} }
/** /**
* Returns a maximum input size for a given mime type, width and height. * Returns a maximum input size for a given codec, mime type, width and height.
* *
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param sampleMimeType The format mime type. * @param sampleMimeType The format mime type.
* @param width The width in pixels. * @param width The width in pixels.
* @param height The height in pixels. * @param height The height in pixels.
* @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
* determined. * determined.
*/ */
private static int getMaxInputSize(String sampleMimeType, int width, int height) { private static int getMaxInputSize(
MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) {
if (width == Format.NO_VALUE || height == Format.NO_VALUE) { if (width == Format.NO_VALUE || height == Format.NO_VALUE) {
// We can't infer a maximum input size without video dimensions. // We can't infer a maximum input size without video dimensions.
return Format.NO_VALUE; return Format.NO_VALUE;
...@@ -1101,9 +1109,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1101,9 +1109,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
minCompressionRatio = 2; minCompressionRatio = 2;
break; break;
case MimeTypes.VIDEO_H264: case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL)) { if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
// The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video || ("Amazon".equals(Util.MANUFACTURER)
// maximum input size, so use the default value. && ("KFSOWI".equals(Util.MODEL) // Kindle Soho
|| ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2
// Use the default value for cases where platform limitations may prevent buffers of the
// calculated maximum input size from being allocated.
return Format.NO_VALUE; return Format.NO_VALUE;
} }
// Round up width/height to an integer number of macroblocks. // Round up width/height to an integer number of macroblocks.
...@@ -1163,14 +1174,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1163,14 +1174,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
} }
/*
* TODO:
*
* 1. Validate that Android device certification now ensures correct behavior, and add a
* corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26).
* 2. Determine a complete list of affected devices.
* 3. Some of the devices in this list only fail to support setOutputSurface when switching from
* a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface),
* and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have
* different pixel formats. If we can find a way to query the Surface instances to determine
* whether this case applies, then we'll be able to provide a more targeted workaround.
*/
/** /**
* Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)}
* incorrectly. * incorrectly.
* <p> *
* If true is returned then we fall back to releasing and re-instantiating the codec instead. * <p>If true is returned then we fall back to releasing and re-instantiating the codec instead.
*
* @param name The name of the codec.
* @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)}
* incorrectly.
*/ */
private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
// Work around https://github.com/google/ExoPlayer/issues/3236, if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) {
// Devices running API level 27 or later should also be unaffected. Google OMX decoders are
// not known to have this issue on any API level.
return false;
}
// Work around:
// https://github.com/google/ExoPlayer/issues/3236,
// https://github.com/google/ExoPlayer/issues/3355, // https://github.com/google/ExoPlayer/issues/3355,
// https://github.com/google/ExoPlayer/issues/3439, // https://github.com/google/ExoPlayer/issues/3439,
// https://github.com/google/ExoPlayer/issues/3724, // https://github.com/google/ExoPlayer/issues/3724,
...@@ -1179,28 +1212,150 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1179,28 +1212,150 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104, // https://github.com/google/ExoPlayer/issues/4104,
// https://github.com/google/ExoPlayer/issues/4134, // https://github.com/google/ExoPlayer/issues/4134,
// https://github.com/google/ExoPlayer/issues/4315. // https://github.com/google/ExoPlayer/issues/4315,
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) // https://github.com/google/ExoPlayer/issues/4419,
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013) // https://github.com/google/ExoPlayer/issues/4460,
|| "mido".equals(Util.DEVICE) // Redmi Note 4 // https://github.com/google/ExoPlayer/issues/4468.
|| "santoni".equals(Util.DEVICE)) // Redmi 4X synchronized (MediaCodecVideoRenderer.class) {
&& "OMX.qcom.video.decoder.avc".equals(name)) if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
|| (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV switch (Util.DEVICE) {
|| "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 case "1601":
|| "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB case "1713":
|| Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus case "1714":
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5 case "A10-70F":
|| "M5c".equals(Util.DEVICE) // Meizu M5C case "A1601":
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E case "A2016a40":
|| "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note case "A7000-a":
|| "woods_f".equals(Util.MODEL) // Moto E (4) case "A7000plus":
|| "watson".equals(Util.DEVICE)) // Moto C case "A7010a48":
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) case "A7020a48":
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite case "AquaPowerM":
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II case "Aura_Note_2":
&& "OMX.k3.video.decoder.avc".equals(name)) case "BLACK-1X":
|| (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite case "BRAVIA_ATV2":
&& "OMX.IMG.MSVDX.Decoder.AVC".equals(name)); case "C1":
case "ComioS1":
case "CP8676_I02":
case "CPH1609":
case "CPY83_I00":
case "cv1":
case "cv3":
case "deb":
case "E5643":
case "ELUGA_A3_Pro":
case "ELUGA_Note":
case "ELUGA_Prim":
case "ELUGA_Ray_X":
case "EverStar_S":
case "F3111":
case "F3113":
case "F3116":
case "F3211":
case "F3213":
case "F3215":
case "F3311":
case "flo":
case "GiONEE_CBL7513":
case "GiONEE_GBL7319":
case "GIONEE_GBL7360":
case "GIONEE_SWW1609":
case "GIONEE_SWW1627":
case "GIONEE_SWW1631":
case "GIONEE_WBL5708":
case "GIONEE_WBL7365":
case "GIONEE_WBL7519":
case "griffin":
case "htc_e56ml_dtul":
case "hwALE-H":
case "HWBLN-H":
case "HWCAM-H":
case "HWVNS-H":
case "iball8735_9806":
case "Infinix-X572":
case "iris60":
case "itel_S41":
case "j2xlteins":
case "JGZ":
case "K50a40":
case "le_x6":
case "LS-5017":
case "M5c":
case "manning":
case "marino_f":
case "MEIZU_M5":
case "mh":
case "mido":
case "MX6":
case "namath":
case "nicklaus_f":
case "NX541J":
case "NX573J":
case "OnePlus5T":
case "p212":
case "P681":
case "P85":
case "panell_d":
case "panell_dl":
case "panell_ds":
case "panell_dt":
case "PB2-670M":
case "PGN528":
case "PGN610":
case "PGN611":
case "Phantom6":
case "Pixi4-7_3G":
case "Pixi5-10_4G":
case "PLE":
case "PRO7S":
case "Q350":
case "Q4260":
case "Q427":
case "Q4310":
case "Q5":
case "QM16XE_U":
case "QX1":
case "santoni":
case "Slate_Pro":
case "SVP-DTV15":
case "s905x018":
case "taido_row":
case "TB3-730F":
case "TB3-730X":
case "TB3-850F":
case "TB3-850M":
case "tcl_eu":
case "V1":
case "V23GB":
case "V5":
case "vernee_M5":
case "watson":
case "whyred":
case "woods_f":
case "woods_fn":
case "X3_HK":
case "XE2X":
case "XT1663":
case "Z12_PRO":
case "Z80":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
}
switch (Util.MODEL) {
case "AFTA":
case "AFTN":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
}
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
}
}
return deviceNeedsSetOutputSurfaceWorkaround;
} }
protected static final class CodecMaxValues { protected static final class CodecMaxValues {
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.EventListener;
...@@ -1981,6 +1982,44 @@ public final class ExoPlayerTest { ...@@ -1981,6 +1982,44 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber()
throws Exception {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 2,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND));
FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSeekToUnpreparedPeriod")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.seek(/* windowIndex= */ 0, /* positionMs= */ 1)
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 1);
assertThat(mediaSource.getCreatedMediaPeriods())
.containsAllOf(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0));
assertThat(mediaSource.getCreatedMediaPeriods())
.doesNotContain(new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1));
}
@Test
public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception {
// We add two listeners to the player. The first stops the player as soon as it's ready and both // We add two listeners to the player. The first stops the player as soon as it's ready and both
// record the state change events they receive. // record the state change events they receive.
...@@ -2040,7 +2079,7 @@ public final class ExoPlayerTest { ...@@ -2040,7 +2079,7 @@ public final class ExoPlayerTest {
final EventListener eventListener = final EventListener eventListener =
new DefaultEventListener() { new DefaultEventListener() {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);
} }
......
...@@ -90,6 +90,19 @@ public final class AdPlaybackStateTest { ...@@ -90,6 +90,19 @@ public final class AdPlaybackStateTest {
} }
@Test @Test
public void testGetFirstAdIndexToPlaySkipsSkippedAd() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1);
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);
}
@Test
public void testGetFirstAdIndexToPlaySkipsErrorAds() { public void testGetFirstAdIndexToPlaySkipsErrorAds() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
......
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.dash; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.dash;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -62,7 +61,6 @@ import java.util.List; ...@@ -62,7 +61,6 @@ import java.util.List;
/* package */ final int id; /* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory; private final DashChunkSource.Factory chunkSourceFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final long elapsedRealtimeOffset; private final long elapsedRealtimeOffset;
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator; private final Allocator allocator;
...@@ -73,6 +71,7 @@ import java.util.List; ...@@ -73,6 +71,7 @@ import java.util.List;
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler> private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
trackEmsgHandlerBySampleStream; trackEmsgHandlerBySampleStream;
private EventDispatcher eventDispatcher;
private @Nullable Callback callback; private @Nullable Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams; private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private EventSampleStream[] eventSampleStreams; private EventSampleStream[] eventSampleStreams;
...@@ -127,6 +126,13 @@ import java.util.List; ...@@ -127,6 +126,13 @@ import java.util.List;
*/ */
public void updateManifest(DashManifest manifest, int periodIndex) { public void updateManifest(DashManifest manifest, int periodIndex) {
this.manifest = manifest; this.manifest = manifest;
if (this.periodIndex != periodIndex) {
eventDispatcher =
eventDispatcher.withParameters(
/* windowIndex= */ 0,
eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex),
manifest.getPeriod(periodIndex).startMs);
}
this.periodIndex = periodIndex; this.periodIndex = periodIndex;
playerEmsgHandler.updateManifest(manifest); playerEmsgHandler.updateManifest(manifest);
if (sampleStreams != null) { if (sampleStreams != null) {
...@@ -139,7 +145,10 @@ import java.util.List; ...@@ -139,7 +145,10 @@ import java.util.List;
for (EventSampleStream eventSampleStream : eventSampleStreams) { for (EventSampleStream eventSampleStream : eventSampleStreams) {
for (EventStream eventStream : eventStreams) { for (EventStream eventStream : eventStreams) {
if (eventStream.id().equals(eventSampleStream.eventStreamId())) { if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
eventSampleStream.updateEventStream(eventStream, manifest.dynamic); int lastPeriodIndex = manifest.getPeriodCount() - 1;
eventSampleStream.updateEventStream(
eventStream,
/* eventStreamAppendable= */ manifest.dynamic && periodIndex == lastPeriodIndex);
break; break;
} }
} }
...@@ -186,126 +195,34 @@ import java.util.List; ...@@ -186,126 +195,34 @@ import java.util.List;
@Override @Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
SparseArray<ChunkSampleStream<DashChunkSource>> primarySampleStreams = new SparseArray<>(); int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections);
List<EventSampleStream> eventSampleStreamList = new ArrayList<>(); releaseDisabledStreams(selections, mayRetainStreamFlags, streams);
releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex);
selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, selectNewStreams(
positionUs, primarySampleStreams); selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex);
selectEventSampleStreams(selections, mayRetainStreamFlags, streams,
streamResetFlags, eventSampleStreamList); ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamList = new ArrayList<>();
selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, ArrayList<EventSampleStream> eventSampleStreamList = new ArrayList<>();
positionUs, primarySampleStreams); for (SampleStream sampleStream : streams) {
if (sampleStream instanceof ChunkSampleStream) {
sampleStreams = newSampleStreamArray(primarySampleStreams.size()); @SuppressWarnings("unchecked")
for (int i = 0; i < sampleStreams.length; i++) { ChunkSampleStream<DashChunkSource> stream =
sampleStreams[i] = primarySampleStreams.valueAt(i); (ChunkSampleStream<DashChunkSource>) sampleStream;
sampleStreamList.add(stream);
} else if (sampleStream instanceof EventSampleStream) {
eventSampleStreamList.add((EventSampleStream) sampleStream);
}
} }
sampleStreams = newSampleStreamArray(sampleStreamList.size());
sampleStreamList.toArray(sampleStreams);
eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()];
eventSampleStreamList.toArray(eventSampleStreams); eventSampleStreamList.toArray(eventSampleStreams);
compositeSequenceableLoader = compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
return positionUs; return positionUs;
} }
private void selectPrimarySampleStreams(
TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs,
SparseArray<ChunkSampleStream<DashChunkSource>> primarySampleStreams) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
if (selections[i] == null || !mayRetainStreamFlags[i]) {
stream.release(this);
streams[i] = null;
} else {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
primarySampleStreams.put(trackGroupIndex, stream);
}
}
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {
ChunkSampleStream<DashChunkSource> stream = buildSampleStream(trackGroupInfo,
selections[i], positionUs);
primarySampleStreams.put(trackGroupIndex, stream);
streams[i] = stream;
streamResetFlags[i] = true;
}
}
}
}
private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags,
List<EventSampleStream> eventSampleStreamsList) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof EventSampleStream) {
EventSampleStream stream = (EventSampleStream) streams[i];
if (selections[i] == null || !mayRetainStreamFlags[i]) {
streams[i] = null;
} else {
eventSampleStreamsList.add(stream);
}
}
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {
EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);
Format format = selections[i].getTrackGroup().getFormat(0);
EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic);
streams[i] = stream;
streamResetFlags[i] = true;
eventSampleStreamsList.add(stream);
}
}
}
}
private void selectEmbeddedSampleStreams(
TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs,
SparseArray<ChunkSampleStream<DashChunkSource>> primarySampleStreams) {
for (int i = 0; i < selections.length; i++) {
if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream)
&& (selections[i] == null || !mayRetainStreamFlags[i])) {
// The stream is for an embedded track and is either no longer selected or needs replacing.
releaseIfEmbeddedSampleStream(streams[i]);
streams[i] = null;
}
// We need to consider replacing the stream even if it's non-null because the primary stream
// may have been replaced, selected or deselected.
if (selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {
ChunkSampleStream<?> primaryStream = primarySampleStreams.get(
trackGroupInfo.primaryTrackGroupIndex);
SampleStream stream = streams[i];
boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream
: (stream instanceof EmbeddedSampleStream
&& ((EmbeddedSampleStream) stream).parent == primaryStream);
if (!mayRetainStream) {
releaseIfEmbeddedSampleStream(stream);
streams[i] = primaryStream == null ? new EmptySampleStream()
: primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);
streamResetFlags[i] = true;
}
}
}
}
}
@Override @Override
public void discardBuffer(long positionUs, boolean toKeyframe) { public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) { for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
...@@ -372,6 +289,124 @@ import java.util.List; ...@@ -372,6 +289,124 @@ import java.util.List;
// Internal methods. // Internal methods.
private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) {
int[] streamIndexToTrackGroupIndex = new int[selections.length];
for (int i = 0; i < selections.length; i++) {
if (selections[i] != null) {
streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup());
} else {
streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET;
}
}
return streamIndexToTrackGroupIndex;
}
private void releaseDisabledStreams(
TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {
for (int i = 0; i < selections.length; i++) {
if (selections[i] == null || !mayRetainStreamFlags[i]) {
if (streams[i] instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream =
(ChunkSampleStream<DashChunkSource>) streams[i];
stream.release(this);
} else if (streams[i] instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) streams[i]).release();
}
streams[i] = null;
}
}
}
private void releaseOrphanEmbeddedStreams(
TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) {
// We need to release an embedded stream if the corresponding primary stream is released.
int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);
boolean mayRetainStream;
if (primaryStreamIndex == C.INDEX_UNSET) {
// If the corresponding primary stream is not selected, we may retain an existing
// EmptySampleStream.
mayRetainStream = streams[i] instanceof EmptySampleStream;
} else {
// If the corresponding primary stream is selected, we may retain the embedded stream if
// the stream's parent still matches.
mayRetainStream =
(streams[i] instanceof EmbeddedSampleStream)
&& ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex];
}
if (!mayRetainStream) {
if (streams[i] instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) streams[i]).release();
}
streams[i] = null;
}
}
}
}
private void selectNewStreams(
TrackSelection[] selections,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs,
int[] streamIndexToTrackGroupIndex) {
// Create newly selected primary and event streams.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
streamResetFlags[i] = true;
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {
streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs);
} else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {
EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);
Format format = selections[i].getTrackGroup().getFormat(0);
streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic);
}
}
}
// Create newly selected embedded streams from the corresponding primary stream. Note that this
// second pass is needed because the primary stream may not have been created yet in a first
// pass if the index of the primary stream is greater than the index of the embedded stream.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {
int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);
if (primaryStreamIndex == C.INDEX_UNSET) {
// If an embedded track is selected without the corresponding primary track, create an
// empty sample stream instead.
streams[i] = new EmptySampleStream();
} else {
streams[i] =
((ChunkSampleStream) streams[primaryStreamIndex])
.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);
}
}
}
}
}
private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) {
int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex];
if (embeddedTrackGroupIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex;
for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) {
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
if (trackGroupIndex == primaryTrackGroupIndex
&& trackGroupInfos[trackGroupIndex].trackGroupCategory
== TrackGroupInfo.CATEGORY_PRIMARY) {
return i;
}
}
return C.INDEX_UNSET;
}
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups( private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
List<AdaptationSet> adaptationSets, List<EventStream> eventStreams) { List<AdaptationSet> adaptationSets, List<EventStream> eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
...@@ -624,12 +659,6 @@ import java.util.List; ...@@ -624,12 +659,6 @@ import java.util.List;
return new ChunkSampleStream[length]; return new ChunkSampleStream[length];
} }
private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) {
if (sampleStream instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) sampleStream).release();
}
}
private static final class TrackGroupInfo { private static final class TrackGroupInfo {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
......
...@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat ...@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat
import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; 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.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
...@@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource {
long availableEndTimeUs = Long.MAX_VALUE; long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false; boolean isIndexExplicit = false;
boolean seenEmptyIndex = false; boolean seenEmptyIndex = false;
boolean haveAudioVideoAdaptationSets = false;
for (int i = 0; i < adaptationSetCount; i++) { for (int i = 0; i < adaptationSetCount; i++) {
DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); int type = period.adaptationSets.get(i).type;
if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) {
haveAudioVideoAdaptationSets = true;
break;
}
}
for (int i = 0; i < adaptationSetCount; i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
// Exclude text adaptation sets from duration calculations, if we have at least one audio
// or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029
if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) {
continue;
}
DashSegmentIndex index = adaptationSet.representations.get(0).getIndex();
if (index == null) { if (index == null) {
return new PeriodSeekInfo(true, 0, durationUs); return new PeriodSeekInfo(true, 0, durationUs);
} }
......
...@@ -36,37 +36,53 @@ import java.io.IOException; ...@@ -36,37 +36,53 @@ import java.io.IOException;
private final EventMessageEncoder eventMessageEncoder; private final EventMessageEncoder eventMessageEncoder;
private long[] eventTimesUs; private long[] eventTimesUs;
private boolean eventStreamUpdatable; private boolean eventStreamAppendable;
private EventStream eventStream; private EventStream eventStream;
private boolean isFormatSentDownstream; private boolean isFormatSentDownstream;
private int currentIndex; private int currentIndex;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) { public EventSampleStream(
EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) {
this.upstreamFormat = upstreamFormat; this.upstreamFormat = upstreamFormat;
this.eventStream = eventStream; this.eventStream = eventStream;
eventMessageEncoder = new EventMessageEncoder(); eventMessageEncoder = new EventMessageEncoder();
pendingSeekPositionUs = C.TIME_UNSET; pendingSeekPositionUs = C.TIME_UNSET;
eventTimesUs = eventStream.presentationTimesUs; eventTimesUs = eventStream.presentationTimesUs;
updateEventStream(eventStream, eventStreamUpdatable); updateEventStream(eventStream, eventStreamAppendable);
} }
void updateEventStream(EventStream eventStream, boolean eventStreamUpdatable) { public String eventStreamId() {
return eventStream.id();
}
public void updateEventStream(EventStream eventStream, boolean eventStreamAppendable) {
long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1]; long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1];
this.eventStreamUpdatable = eventStreamUpdatable; this.eventStreamAppendable = eventStreamAppendable;
this.eventStream = eventStream; this.eventStream = eventStream;
this.eventTimesUs = eventStream.presentationTimesUs; this.eventTimesUs = eventStream.presentationTimesUs;
if (pendingSeekPositionUs != C.TIME_UNSET) { if (pendingSeekPositionUs != C.TIME_UNSET) {
seekToUs(pendingSeekPositionUs); seekToUs(pendingSeekPositionUs);
} else if (lastReadPositionUs != C.TIME_UNSET) { } else if (lastReadPositionUs != C.TIME_UNSET) {
currentIndex = Util.binarySearchCeil(eventTimesUs, lastReadPositionUs, false, false); currentIndex =
Util.binarySearchCeil(
eventTimesUs, lastReadPositionUs, /* inclusive= */ false, /* stayInBounds= */ false);
} }
} }
String eventStreamId() { /**
return eventStream.id(); * Seeks to the specified position in microseconds.
*
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long positionUs) {
currentIndex =
Util.binarySearchCeil(
eventTimesUs, positionUs, /* inclusive= */ true, /* stayInBounds= */ false);
boolean isPendingSeek = eventStreamAppendable && currentIndex == eventTimesUs.length;
pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;
} }
@Override @Override
...@@ -88,7 +104,7 @@ import java.io.IOException; ...@@ -88,7 +104,7 @@ import java.io.IOException;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
if (currentIndex == eventTimesUs.length) { if (currentIndex == eventTimesUs.length) {
if (!eventStreamUpdatable) { if (!eventStreamAppendable) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} else { } else {
...@@ -118,15 +134,4 @@ import java.io.IOException; ...@@ -118,15 +134,4 @@ import java.io.IOException;
return skipped; return skipped;
} }
/**
* Seeks to the specified position in microseconds.
*
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long positionUs) {
currentIndex = Util.binarySearchCeil(eventTimesUs, positionUs, true, false);
boolean isPendingSeek = eventStreamUpdatable && currentIndex == eventTimesUs.length;
pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;
}
} }
...@@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler
protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp) protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String schemeType = null; String schemeType = null;
String licenseServerUrl = null;
byte[] data = null; byte[] data = null;
UUID uuid = null; UUID uuid = null;
boolean requiresSecureDecoder = false; boolean requiresSecureDecoder = false;
...@@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler
switch (Util.toLowerInvariant(schemeIdUri)) { switch (Util.toLowerInvariant(schemeIdUri)) {
case "urn:mpeg:dash:mp4protection:2011": case "urn:mpeg:dash:mp4protection:2011":
schemeType = xpp.getAttributeValue(null, "value"); schemeType = xpp.getAttributeValue(null, "value");
String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID");
if (!TextUtils.isEmpty(defaultKid) if (!TextUtils.isEmpty(defaultKid)
&& !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) {
String[] defaultKidStrings = defaultKid.split("\\s+"); String[] defaultKidStrings = defaultKid.split("\\s+");
...@@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler ...@@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler
do { do {
xpp.next(); xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl");
} else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) {
String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); String robustnessLevel = xpp.getAttributeValue(null, "robustness_level");
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
} else if (data == null) { } else if (data == null) {
if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { if (XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh")
&& xpp.next() == XmlPullParser.TEXT) {
// The cenc:pssh element is defined in 23001-7:2015. // The cenc:pssh element is defined in 23001-7:2015.
data = Base64.decode(xpp.getText(), Base64.DEFAULT); data = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(data); uuid = PsshAtomUtil.parseUuid(data);
...@@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler ...@@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler
} }
} }
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
SchemeData schemeData = uuid != null SchemeData schemeData =
? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; uuid != null
? new SchemeData(
uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
: null;
return Pair.create(schemeType, schemeData); return Pair.create(schemeType, schemeData);
} }
......
...@@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public void releaseSourceInternal() { public void releaseSourceInternal() {
if (playlistTracker != null) { if (playlistTracker != null) {
playlistTracker.release(); playlistTracker.stop();
} }
} }
......
...@@ -136,6 +136,7 @@ import java.util.Arrays; ...@@ -136,6 +136,7 @@ import java.util.Arrays;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private boolean tracksEnded; private boolean tracksEnded;
private long sampleOffsetUs; private long sampleOffsetUs;
private int chunkUid;
/** /**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
...@@ -650,6 +651,7 @@ import java.util.Arrays; ...@@ -650,6 +651,7 @@ import java.util.Arrays;
audioSampleQueueMappingDone = false; audioSampleQueueMappingDone = false;
videoSampleQueueMappingDone = false; videoSampleQueueMappingDone = false;
} }
this.chunkUid = chunkUid;
for (SampleQueue sampleQueue : sampleQueues) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(chunkUid); sampleQueue.sourceId(chunkUid);
} }
...@@ -704,6 +706,7 @@ import java.util.Arrays; ...@@ -704,6 +706,7 @@ import java.util.Arrays;
} }
} }
SampleQueue trackOutput = new SampleQueue(allocator); SampleQueue trackOutput = new SampleQueue(allocator);
trackOutput.sourceId(chunkUid);
trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.setUpstreamFormatChangeListener(this); trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
......
...@@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker ...@@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker
} }
@Override @Override
public void release() { public void stop() {
primaryHlsUrl = null; primaryHlsUrl = null;
primaryUrlSnapshot = null; primaryUrlSnapshot = null;
masterPlaylist = null; masterPlaylist = null;
......
...@@ -100,8 +100,8 @@ public interface HlsPlaylistTracker { ...@@ -100,8 +100,8 @@ public interface HlsPlaylistTracker {
/** /**
* Starts the playlist tracker. * Starts the playlist tracker.
* *
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link * <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
* #release()} call. * call.
* *
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist. * playlist.
...@@ -111,8 +111,12 @@ public interface HlsPlaylistTracker { ...@@ -111,8 +111,12 @@ public interface HlsPlaylistTracker {
void start( void start(
Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);
/** Releases all acquired resources. Must be called once per {@link #start} call. */ /**
void release(); * Stops the playlist tracker and releases any acquired resources.
*
* <p>Must be called once per {@link #start} call.
*/
void stop();
/** /**
* Registers a listener to receive events from the playlist tracker. * Registers a listener to receive events from the playlist tracker.
......
...@@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; ...@@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException; import java.io.IOException;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
...@@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner; ...@@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class Aes128DataSourceTest { public class Aes128DataSourceTest {
@Ignore
@Test @Test
public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException { public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = new UpstreamDataSource(); UpstreamDataSource upstream = new UpstreamDataSource();
...@@ -44,6 +47,7 @@ public class Aes128DataSourceTest { ...@@ -44,6 +47,7 @@ public class Aes128DataSourceTest {
assertThat(upstream.opened).isFalse(); assertThat(upstream.opened).isFalse();
} }
@Ignore
@Test @Test
public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException { public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = UpstreamDataSource upstream =
......
...@@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
private static final int DEFAULT_INCREMENT_COUNT = 20; private static final int DEFAULT_INCREMENT_COUNT = 20;
/**
* The name of the Android SDK view that most closely resembles this custom view. Used as the
* class name for accessibility.
*/
private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar";
private final Rect seekBounds; private final Rect seekBounds;
private final Rect progressBar; private final Rect progressBar;
private final Rect bufferedBar; private final Rect bufferedBar;
...@@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Paint adMarkerPaint; private final Paint adMarkerPaint;
private final Paint playedAdMarkerPaint; private final Paint playedAdMarkerPaint;
private final Paint scrubberPaint; private final Paint scrubberPaint;
private final Drawable scrubberDrawable; private final @Nullable Drawable scrubberDrawable;
private final int barHeight; private final int barHeight;
private final int touchTargetHeight; private final int touchTargetHeight;
private final int adMarkerWidth; private final int adMarkerWidth;
...@@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Formatter formatter; private final Formatter formatter;
private final Runnable stopScrubbingRunnable; private final Runnable stopScrubbingRunnable;
private final CopyOnWriteArraySet<OnScrubListener> listeners; private final CopyOnWriteArraySet<OnScrubListener> listeners;
private final int[] locationOnScreen;
private final Point touchPosition;
private int keyCountIncrement; private int keyCountIncrement;
private long keyTimeIncrement; private long keyTimeIncrement;
private int lastCoarseScrubXPosition; private int lastCoarseScrubXPosition;
private int[] locationOnScreen;
private Point touchPosition;
private boolean scrubbing; private boolean scrubbing;
private long scrubPosition; private long scrubPosition;
...@@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar {
private long position; private long position;
private long bufferedPosition; private long bufferedPosition;
private int adGroupCount; private int adGroupCount;
private long[] adGroupTimesMs; private @Nullable long[] adGroupTimesMs;
private boolean[] playedAdGroups; private @Nullable boolean[] playedAdGroups;
/** /** Creates a new time bar. */
* Creates a new time bar.
*/
public DefaultTimeBar(Context context, AttributeSet attrs) { public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
seekBounds = new Rect(); seekBounds = new Rect();
...@@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberPaint = new Paint(); scrubberPaint = new Paint();
scrubberPaint.setAntiAlias(true); scrubberPaint.setAntiAlias(true);
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
locationOnScreen = new int[2];
touchPosition = new Point();
// Calculate the dimensions and paints for drawn elements. // Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources(); Resources res = context.getResources();
...@@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText()); event.getText().add(getProgressText());
} }
event.setClassName(DefaultTimeBar.class.getName()); event.setClassName(ACCESSIBILITY_CLASS_NAME);
} }
@TargetApi(21) @TargetApi(21)
@Override @Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info); super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(DefaultTimeBar.class.getCanonicalName()); info.setClassName(ACCESSIBILITY_CLASS_NAME);
info.setContentDescription(getProgressText()); info.setContentDescription(getProgressText());
if (duration <= 0) { if (duration <= 0) {
return; return;
...@@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar {
@TargetApi(16) @TargetApi(16)
@Override @Override
public boolean performAccessibilityAction(int action, Bundle args) { public boolean performAccessibilityAction(int action, @Nullable Bundle args) {
if (super.performAccessibilityAction(action, args)) { if (super.performAccessibilityAction(action, args)) {
return true; return true;
} }
...@@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar {
} }
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
if (locationOnScreen == null) {
locationOnScreen = new int[2];
touchPosition = new Point();
}
getLocationOnScreen(locationOnScreen); getLocationOnScreen(locationOnScreen);
touchPosition.set( touchPosition.set(
((int) motionEvent.getRawX()) - locationOnScreen[0], ((int) motionEvent.getRawX()) - locationOnScreen[0],
...@@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar {
if (scrubberBar.width() > 0) { if (scrubberBar.width() > 0) {
canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint);
} }
if (adGroupCount == 0) {
return;
}
long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs);
boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups);
int adMarkerOffset = adMarkerWidth / 2; int adMarkerOffset = adMarkerWidth / 2;
for (int i = 0; i < adGroupCount; i++) { for (int i = 0; i < adGroupCount; i++) {
long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);
......
...@@ -55,10 +55,18 @@ public final class DownloadNotificationUtil { ...@@ -55,10 +55,18 @@ public final class DownloadNotificationUtil {
int downloadTaskCount = 0; int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false; boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false;
for (TaskState taskState : taskStates) { for (TaskState taskState : taskStates) {
if (taskState.action.isRemoveAction || taskState.state != TaskState.STATE_STARTED) { if (taskState.state != TaskState.STATE_STARTED
&& taskState.state != TaskState.STATE_COMPLETED) {
continue; continue;
} }
if (taskState.action.isRemoveAction) {
haveRemoveTasks = true;
continue;
}
haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false; allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage; totalPercentage += taskState.downloadPercentage;
...@@ -67,18 +75,20 @@ public final class DownloadNotificationUtil { ...@@ -67,18 +75,20 @@ public final class DownloadNotificationUtil {
downloadTaskCount++; downloadTaskCount++;
} }
boolean haveDownloadTasks = downloadTaskCount > 0;
int titleStringId = int titleStringId =
haveDownloadTasks haveDownloadTasks
? R.string.exo_download_downloading ? R.string.exo_download_downloading
: (taskStates.length > 0 ? R.string.exo_download_removing : NULL_STRING_ID); : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID);
NotificationCompat.Builder notificationBuilder = NotificationCompat.Builder notificationBuilder =
newNotificationBuilder( newNotificationBuilder(
context, smallIcon, channelId, contentIntent, message, titleStringId); context, smallIcon, channelId, contentIntent, message, titleStringId);
int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0; int progress = 0;
boolean indeterminate = boolean indeterminate = true;
!haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes); if (haveDownloadTasks) {
progress = (int) (totalPercentage / downloadTaskCount);
indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
}
notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
notificationBuilder.setOngoing(true); notificationBuilder.setOngoing(true);
notificationBuilder.setShowWhen(false); notificationBuilder.setShowWhen(false);
......
...@@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout {
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
updateNavigation(); updateNavigation();
updateTimeBarMode(); updateTimeBarMode();
updateProgress(); updateProgress();
......
...@@ -949,7 +949,7 @@ public class PlayerNotificationManager { ...@@ -949,7 +949,7 @@ public class PlayerNotificationManager {
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return; return;
} }
......
...@@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout { ...@@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout {
return useController && controller.dispatchMediaKeyEvent(event); return useController && controller.dispatchMediaKeyEvent(event);
} }
/** Returns whether the controller is currently visible. */
public boolean isControllerVisible() {
return controller != null && controller.isVisible();
}
/** /**
* Shows the playback controls. Does nothing if playback controls are disabled. * Shows the playback controls. Does nothing if playback controls are disabled.
* *
......
...@@ -28,6 +28,7 @@ import android.graphics.Rect; ...@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.text.Layout.Alignment; import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util;
private int edgeColor; private int edgeColor;
@CaptionStyleCompat.EdgeType @CaptionStyleCompat.EdgeType
private int edgeType; private int edgeType;
private float textSizePx; private float defaultTextSizePx;
private float cueTextSizePx;
private float bottomPaddingFraction; private float bottomPaddingFraction;
private int parentLeft; private int parentLeft;
private int parentTop; private int parentTop;
...@@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util;
/** /**
* Draws the provided {@link Cue} into a canvas with the specified styling. * Draws the provided {@link Cue} into a canvas with the specified styling.
* <p> *
* A call to this method is able to use cached results of calculations made during the previous * <p>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 * call, and so an instance of this class is able to optimize repeated calls to this method in
* which the same parameters are passed. * which the same parameters are passed.
* *
...@@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util;
* @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font
* sizes embedded within the cue should be applied. Otherwise, it is ignored. * sizes embedded within the cue should be applied. Otherwise, it is ignored.
* @param style The style to use when drawing the cue text. * @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 defaultTextSizePx The default text size to use when drawing the text, in pixels.
* @param cueTextSizePx The embedded text size of this cue, in pixels.
* @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw. * @param canvas The canvas into which to draw.
...@@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util;
* @param cueBoxRight The right 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. * @param cueBoxBottom The bottom position of the enclosing cue box.
*/ */
public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, public void draw(
CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, Cue cue,
int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { boolean applyEmbeddedStyles,
boolean applyEmbeddedFontSizes,
CaptionStyleCompat style,
float defaultTextSizePx,
float cueTextSizePx,
float bottomPaddingFraction,
Canvas canvas,
int cueBoxLeft,
int cueBoxTop,
int cueBoxRight,
int cueBoxBottom) {
boolean isTextCue = cue.bitmap == null; boolean isTextCue = cue.bitmap == null;
int windowColor = Color.BLACK; int windowColor = Color.BLACK;
if (isTextCue) { if (isTextCue) {
...@@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util;
&& this.edgeType == style.edgeType && this.edgeType == style.edgeType
&& this.edgeColor == style.edgeColor && this.edgeColor == style.edgeColor
&& Util.areEqual(this.textPaint.getTypeface(), style.typeface) && Util.areEqual(this.textPaint.getTypeface(), style.typeface)
&& this.textSizePx == textSizePx && this.defaultTextSizePx == defaultTextSizePx
&& this.cueTextSizePx == cueTextSizePx
&& this.bottomPaddingFraction == bottomPaddingFraction && this.bottomPaddingFraction == bottomPaddingFraction
&& this.parentLeft == cueBoxLeft && this.parentLeft == cueBoxLeft
&& this.parentTop == cueBoxTop && this.parentTop == cueBoxTop
...@@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util;
this.edgeType = style.edgeType; this.edgeType = style.edgeType;
this.edgeColor = style.edgeColor; this.edgeColor = style.edgeColor;
this.textPaint.setTypeface(style.typeface); this.textPaint.setTypeface(style.typeface);
this.textSizePx = textSizePx; this.defaultTextSizePx = defaultTextSizePx;
this.cueTextSizePx = cueTextSizePx;
this.bottomPaddingFraction = bottomPaddingFraction; this.bottomPaddingFraction = bottomPaddingFraction;
this.parentLeft = cueBoxLeft; this.parentLeft = cueBoxLeft;
this.parentTop = cueBoxTop; this.parentTop = cueBoxTop;
...@@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util;
int parentWidth = parentRight - parentLeft; int parentWidth = parentRight - parentLeft;
int parentHeight = parentBottom - parentTop; int parentHeight = parentBottom - parentTop;
textPaint.setTextSize(textSizePx); textPaint.setTextSize(defaultTextSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f); int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2; int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) { if (cueSize != Cue.DIMEN_UNSET) {
...@@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util; ...@@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util;
return; return;
} }
CharSequence cueText = this.cueText;
// Remove embedded styling or font size if requested. // Remove embedded styling or font size if requested.
CharSequence cueText; if (!applyEmbeddedStyles) {
if (applyEmbeddedFontSizes && applyEmbeddedStyles) { cueText = cueText.toString(); // Equivalent to erasing all spans.
cueText = this.cueText; } else if (!applyEmbeddedFontSizes) {
} else if (!applyEmbeddedStyles) { SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
cueText = this.cueText.toString(); // Equivalent to erasing all spans.
} else {
SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);
int cueLength = newCueText.length(); int cueLength = newCueText.length();
AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);
RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);
...@@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util;
newCueText.removeSpan(relSpan); newCueText.removeSpan(relSpan);
} }
cueText = newCueText; cueText = newCueText;
} else {
// Apply embedded styles & font size.
if (cueTextSizePx > 0) {
// Use a SpannableStringBuilder encompassing the whole cue text to apply the default
// cueTextSizePx.
SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
newCueText.setSpan(
new AbsoluteSizeSpan((int) cueTextSizePx),
/* start= */ 0,
/* end= */ newCueText.length(),
Spanned.SPAN_PRIORITY);
cueText = newCueText;
}
} }
Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
......
...@@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput {
for (int i = 0; i < cueCount; i++) { for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i); Cue cue = cues.get(i);
float textSizePx = float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
SubtitlePainter painter = painters.get(i); SubtitlePainter painter = painters.get(i);
painter.draw( painter.draw(
cue, cue,
applyEmbeddedStyles, applyEmbeddedStyles,
applyEmbeddedFontSizes, applyEmbeddedFontSizes,
style, style,
textSizePx, defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction, bottomPaddingFraction,
canvas, canvas,
left, left,
...@@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput {
} }
} }
private float resolveTextSizeForCue( private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx; return 0;
} }
float defaultCueTextSizePx = float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; return Math.max(defaultCueTextSizePx, 0);
} }
private float resolveTextSize( private float resolveTextSize(
......
...@@ -575,7 +575,9 @@ public abstract class Action { ...@@ -575,7 +575,9 @@ public abstract class Action {
new Player.DefaultEventListener() { new Player.DefaultEventListener() {
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline,
@Nullable Object manifest,
@Player.TimelineChangeReason int reason) {
if (expectedTimeline == null || timeline.equals(expectedTimeline)) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) {
player.removeListener(this); player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler); nextAction.schedule(player, trackSelector, surface, handler);
......
...@@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener ...@@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
// Player.EventListener // Player.EventListener
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
timelines.add(timeline); timelines.add(timeline);
manifests.add(manifest); manifests.add(manifest);
timelineChangeReasons.add(reason); timelineChangeReasons.add(reason);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment