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

r2.8.3
This commit is contained in:
ojw28 2018-08-07 15:50:14 +01:00 committed by GitHub
commit 8cbcfd669d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1956 additions and 709 deletions

495
.idea/codeStyleSettings.xml Normal file
View file

@ -0,0 +1,495 @@
<?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>

View file

@ -1,5 +1,48 @@
# 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 ###
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.8.2'
releaseVersionCode = 2802
releaseVersion = '2.8.3'
releaseVersionCode = 2803
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
@ -282,7 +283,7 @@ import java.util.ArrayList;
@Override
public void onTimelineChanged(
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (timeline.isEmpty()) {
castMediaQueueCreationPending = true;

View file

@ -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.AdsRenderingSettings;
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.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
@ -62,6 +63,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The expected ad group index that IMA should load next. */
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;
/**
* 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;
/** The current ad playback state. */
private @ImaAdState int imaAdState;
@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state.
/**
* Whether the player is playing an ad.
*/
/** Whether the player is playing an ad. */
private boolean playingAd;
/**
* 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
* content progress should increase. {@link C#TIME_UNSET} otherwise.
*/
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;
/**
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
private boolean sentPendingContentPositionMs;
/**
@ -405,6 +397,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adsLoader;
}
/**
* 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.
*
@ -509,6 +512,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.destroy();
adsManager = null;
}
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE;
updateAdPlaybackState();
}
@Override
@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent);
Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
return;
}
try {
@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void loadAd(String adUriString) {
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) {
Log.w(
TAG,
@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
}
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
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
if (DEBUG) {
Log.d(TAG, "playAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring playAd after release");
return;
}
switch (imaAdState) {
case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content.
@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) {
Log.d(TAG, "stopAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring stopAd after release");
return;
}
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected stopAd while detached");
@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Player.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released.
return;
@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(
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) {
// 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.
@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState();
adPlaybackState = AdPlaybackState.NONE;
} else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);

View file

@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);

View file

@ -674,8 +674,8 @@ public final class MediaSessionConnector {
private int currentWindowCount;
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {

View file

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** 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.
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}. */
// 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.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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}

View file

@ -80,6 +80,13 @@ public final class Format implements Parcelable {
/** DRM initialization data if the stream is protected, or null otherwise. */
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.
/**
@ -141,15 +148,6 @@ public final class Format implements Parcelable {
*/
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.
/**

View file

@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions;
reading = playing.next;
}
playing.release();
playing = playing.next;
length--;
if (length == 0) {
loading = null;
oldFrontPeriodUid = playing.uid;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
}
playing = playing.next;
} else {
playing = loading;
reading = loading;

View file

@ -191,7 +191,8 @@ public interface Player {
* @param manifest The latest manifest. May be null.
* @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.
@ -281,8 +282,8 @@ public interface Player {
abstract class DefaultEventListener implements EventListener {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
// Call deprecated version. Otherwise, do nothing.
onTimelineChanged(timeline, manifest);
}
@ -337,7 +338,7 @@ public interface Player {
* instead.
*/
@Deprecated
public void onTimelineChanged(Timeline timeline, Object manifest) {
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing.
}

View file

@ -420,7 +420,7 @@ public class AnalyticsCollector
@Override
public final void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {

View file

@ -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
* 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
* 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
* specified value will be rounded so that the threshold check only depends on the more

View file

@ -22,11 +22,12 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
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.ProvisionRequest;
import java.util.Arrays;
@ -77,8 +78,7 @@ import java.util.UUID;
private final ExoMediaDrm<T> mediaDrm;
private final ProvisioningManager<T> provisioningManager;
private final byte[] initData;
private final String mimeType;
private final SchemeData schemeData;
private final @DefaultDrmSessionManager.Mode int mode;
private final HashMap<String, String> optionalKeyRequestParameters;
private final EventDispatcher eventDispatcher;
@ -97,15 +97,20 @@ import java.util.UUID;
private byte[] sessionId;
private byte[] offlineLicenseKeySetId;
private Object currentKeyRequest;
private Object currentProvisionRequest;
/**
* Instantiates a new DRM session.
*
* @param uuid The UUID of the drm scheme.
* @param mediaDrm The media DRM.
* @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 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 callback The media DRM callback.
* @param playbackLooper The playback looper.
@ -117,10 +122,9 @@ import java.util.UUID;
UUID uuid,
ExoMediaDrm<T> mediaDrm,
ProvisioningManager<T> provisioningManager,
byte[] initData,
String mimeType,
@Nullable SchemeData schemeData,
@DefaultDrmSessionManager.Mode int mode,
byte[] offlineLicenseKeySetId,
@Nullable byte[] offlineLicenseKeySetId,
HashMap<String, String> optionalKeyRequestParameters,
MediaDrmCallback callback,
Looper playbackLooper,
@ -131,6 +135,7 @@ import java.util.UUID;
this.mediaDrm = mediaDrm;
this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
this.schemeData = offlineLicenseKeySetId == null ? schemeData : null;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.callback = callback;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
@ -141,14 +146,6 @@ import java.util.UUID;
requestHandlerThread = new HandlerThread("DrmRequestHandler");
requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
if (offlineLicenseKeySetId == null) {
this.initData = initData;
this.mimeType = mimeType;
} else {
this.initData = null;
this.mimeType = null;
}
}
// Life cycle.
@ -177,6 +174,8 @@ import java.util.UUID;
requestHandlerThread = null;
mediaCrypto = null;
lastException = null;
currentKeyRequest = null;
currentProvisionRequest = null;
if (sessionId != null) {
mediaDrm.closeSession(sessionId);
sessionId = null;
@ -187,18 +186,42 @@ import java.util.UUID;
}
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) {
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.
public void provision() {
ProvisionRequest request = mediaDrm.getProvisionRequest();
postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget();
currentProvisionRequest = mediaDrm.getProvisionRequest();
postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true);
}
public void onProvisionCompleted() {
@ -271,11 +294,12 @@ import java.util.UUID;
return false;
}
private void onProvisionResponse(Object response) {
if (state != STATE_OPENING && !isOpen()) {
private void onProvisionResponse(Object request, Object response) {
if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) {
// This event is stale.
return;
}
currentProvisionRequest = null;
if (response instanceof Exception) {
provisioningManager.onProvisionError((Exception) response);
@ -356,24 +380,30 @@ import java.util.UUID;
private void postKeyRequest(int type, boolean allowRetry) {
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 {
KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
optionalKeyRequestParameters);
if (C.CLEARKEY_UUID.equals(uuid)) {
request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()),
request.getDefaultUrl());
}
postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
KeyRequest mediaDrmKeyRequest =
mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters);
currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl);
postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry);
} catch (Exception e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response) {
if (!isOpen()) {
private void onKeyResponse(Object request, Object response) {
if (request != currentKeyRequest || !isOpen()) {
// This event is stale.
return;
}
currentKeyRequest = null;
if (response instanceof Exception) {
onKeysError((Exception) response);
@ -382,9 +412,6 @@ import java.util.UUID;
try {
byte[] responseData = (byte[]) response;
if (C.CLEARKEY_UUID.equals(uuid)) {
responseData = ClearKeyUtil.adjustResponseData(responseData);
}
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
eventDispatcher.drmKeysRemoved();
@ -430,30 +457,7 @@ import java.util.UUID;
return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
}
@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;
}
}
// Internal classes.
@SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
@ -464,12 +468,15 @@ import java.util.UUID;
@Override
public void handleMessage(Message msg) {
Pair<?, ?> requestAndResponse = (Pair<?, ?>) msg.obj;
Object request = requestAndResponse.first;
Object response = requestAndResponse.second;
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
onProvisionResponse(request, response);
break;
case MSG_KEYS:
onKeyResponse(msg.obj);
onKeyResponse(request, response);
break;
default:
break;
@ -486,21 +493,27 @@ import java.util.UUID;
super(backgroundLooper);
}
Message obtainMessage(int what, Object object, boolean allowRetry) {
return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */,
object);
void post(int what, Object request, boolean allowRetry) {
int allowRetryInt = allowRetry ? 1 : 0;
int errorCount = 0;
obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget();
}
@Override
@SuppressWarnings("unchecked")
public void handleMessage(Message msg) {
Object request = msg.obj;
Object response;
try {
switch (msg.what) {
case MSG_PROVISION:
response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request);
break;
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;
default:
throw new RuntimeException();
@ -511,7 +524,7 @@ import java.util.UUID;
}
response = e;
}
postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget();
}
private boolean maybeRetryRequest(Message originalMsg) {
@ -534,5 +547,4 @@ import java.util.UUID;
}
}
}

View file

@ -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.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
private static final String TAG = "DefaultDrmSessionMgr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid;
private final ExoMediaDrm<T> mediaDrm;
@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
}
}
byte[] initData = null;
String mimeType = null;
SchemeData schemeData = null;
if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid, false);
if (data == null) {
schemeData = getSchemeData(drmInitData, uuid, false);
if (schemeData == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
eventDispatcher.drmSessionManagerError(error);
return new ErrorStateDrmSession<>(new DrmSessionException(error));
}
initData = getSchemeInitData(data, uuid);
mimeType = getSchemeMimeType(data, uuid);
}
DefaultDrmSession<T> session;
@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} else {
// Only use an existing session if it has matching init data.
session = null;
byte[] initData = schemeData != null ? schemeData.data : null;
for (DefaultDrmSession<T> existingSession : sessions) {
if (existingSession.hasInitData(initData)) {
session = existingSession;
@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
uuid,
mediaDrm,
this,
initData,
mimeType,
schemeData,
mode,
offlineLicenseKeySetId,
optionalKeyRequestParameters,
@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
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")
private class MediaDrmHandler extends Handler {

View file

@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* applies to all schemes).
*/
private final UUID uuid;
/**
* The mimeType of {@link #data}.
*/
/** The URL of the server to which license requests should be made. May be null if unknown. */
public final @Nullable String licenseServerUrl;
/** The mimeType of {@link #data}. */
public final String mimeType;
/**
* The initialization data. May be null for scheme support checks only.
@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param requiresSecureDecryption See {@link #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.licenseServerUrl = licenseServerUrl;
this.mimeType = Assertions.checkNotNull(mimeType);
this.data = data;
this.requiresSecureDecryption = requiresSecureDecryption;
@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
/* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong());
licenseServerUrl = in.readString();
mimeType = in.readString();
data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0;
@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return true;
}
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);
}
@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public int hashCode() {
if (hashCode == 0) {
int result = uuid.hashCode();
result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data);
hashCode = result;
@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits());
dest.writeString(licenseServerUrl);
dest.writeString(mimeType);
dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.HashMap;
@ -40,6 +43,8 @@ import java.util.UUID;
@TargetApi(23)
public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid;
private final MediaDrm mediaDrm;
@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
}
}
@SuppressLint("WrongConstant")
private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
@ -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;
this.uuid = uuid;
this.mediaDrm = new MediaDrm(uuid);
if (C.WIDEVINE_UUID.equals(uuid) && needsForceL3Workaround()) {
mediaDrm.setPropertyString("securityLevel", "L3");
}
}
@Override
@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override
public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
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,
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
public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response);
}
return mediaDrm.provideKeyResponse(scope, response);
}
@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
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);
}
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
}
@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();
if (TextUtils.isEmpty(url)) {
url = mediaProvidedLicenseServerUrl;
}
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl;
}

View file

@ -15,6 +15,7 @@
*/
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.ProvisionRequest;
import com.google.android.exoplayer2.util.Assertions;
@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback {
}
@Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
return keyResponse;
}

View file

@ -15,6 +15,7 @@
*/
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.ProvisionRequest;
import java.util.UUID;
@ -38,10 +39,13 @@ public interface MediaDrmCallback {
* Executes a key request.
*
* @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.
* @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;
}

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
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.Pattern;
@ -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 =
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 {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
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;
}
}
@ -103,14 +113,10 @@ public final class GaplessInfoHolder {
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero.
*
* @param name The comment's identifier.
* @param data The comment's payload data.
* @return Whether the holder was populated.
*/
private boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}
private boolean setFromComment(String data) {
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) {
try {

View file

@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.number = (int) value;
break;
case ID_FLAG_DEFAULT:
currentTrack.flagForced = value == 1;
currentTrack.flagDefault = value == 1;
break;
case ID_FLAG_FORCED:
currentTrack.flagDefault = value == 1;
currentTrack.flagForced = value == 1;
break;
case ID_TRACK_TYPE:
currentTrack.type = (int) value;

View file

@ -43,6 +43,9 @@ import java.util.List;
*/
/* 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 int TYPE_vide = Util.getIntegerCodeForString("vide");
@ -117,10 +120,12 @@ import java.util.List;
* @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information.
* @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,
GaplessInfoHolder gaplessInfoHolder) throws ParserException {
public static TrackSampleTable parseStbl(
Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
throws ParserException {
SampleSizeBox sampleSizeBox;
Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) {
@ -136,7 +141,13 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) {
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.
@ -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.
// This implementation does not support applying both gapless metadata and an edit list.
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
@ -342,7 +354,8 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
}
}
@ -359,7 +372,8 @@ import java.util.List;
}
durationUs =
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.
@ -409,6 +423,11 @@ import java.util.List;
System.arraycopy(sizes, startIndex, editedSizes, 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++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs =
@ -424,20 +443,8 @@ import java.util.List;
pts += editDuration;
}
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(
track,
editedOffsets,
editedSizes,
editedMaximumSize,

View file

@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
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);
durationUs = Math.max(durationUs, track.durationUs);
}
@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor {
Assertions.checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; 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 {
parseMoof(moof, trackBundles, flags, extendedTypeScratch);
// If drm init data is sideloaded, we ignore pssh boxes.
@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) {
return;
}
@ -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
* does not refer to any {@link TrackBundle}.
*/
private static TrackBundle parseTfhd(ParsableByteArray tfhd,
SparseArray<TrackBundle> trackBundles, int flags) {
private static TrackBundle parseTfhd(
ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) {
tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom);
int trackId = tfhd.readInt();
TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0);
TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
if (trackBundle == null) {
return null;
}
@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor {
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).
*

View file

@ -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.CommentFrame;
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.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(atomSize - 12);
}
}
if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
// We're only interested in iTunSMPB.
if (domain == null || name == null || dataAtomPosition == -1) {
return null;
}
data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(dataAtomSize - 16);
return new CommentFrame(LANGUAGE_UNDEFINED, name, value);
return new InternalFrame(domain, name, value);
}
private static int parseUint8AttributeValue(ParsableByteArray data) {

View file

@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
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),
C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, 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;
}
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
ArrayList<TrackSampleTable> trackSampleTables;
try {
trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
} catch (AtomParsers.UnhandledEditListException e) {
// Discard gapless info as we aren't able to handle corresponding edits.
gaplessInfoHolder = new GaplessInfoHolder();
trackSampleTables =
getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true);
}
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,
extractorOutput.track(i, track.type));
// 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 {
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.
* <p>

View file

@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class TrackSampleTable {
/**
* Number of samples.
*/
/** The track corresponding to this sample table. */
public final Track track;
/** Number of samples. */
public final int sampleCount;
/**
* Sample offsets in bytes.
*/
/** Sample offsets in bytes. */
public final long[] offsets;
/**
* Sample sizes in bytes.
*/
/** Sample sizes in bytes. */
public final int[] sizes;
/**
* Maximum sample size in {@link #sizes}.
*/
/** Maximum sample size in {@link #sizes}. */
public final int maximumSize;
/**
* Sample timestamps in microseconds.
*/
/** Sample timestamps in microseconds. */
public final long[] timestampsUs;
/**
* Sample flags.
*/
/** Sample flags. */
public final int[] flags;
/**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
public final long durationUs;
public TrackSampleTable(
Track track,
long[] offsets,
int[] sizes,
int maximumSize,
@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
this.track = track;
this.offsets = offsets;
this.sizes = sizes;
this.maximumSize = maximumSize;

View file

@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor {
private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
// Max search length for first audio and video track in input data.
private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
// Max search length for additional audio and video tracks in input data after at least one audio
// and video track has been found.
private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD;
public static final int AUDIO_STREAM = 0xC0;
@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor {
private boolean foundAllTracks;
private boolean foundAudioTrack;
private boolean foundVideoTrack;
private long lastTrackPosition;
// Accessed only by the loading thread.
private ExtractorOutput output;
@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) {
if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null;
if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs.
elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true;
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
lastTrackPosition = input.getPosition();
} else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true;
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
lastTrackPosition = input.getPosition();
} else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader();
foundVideoTrack = true;
lastTrackPosition = input.getPosition();
}
if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor {
psPayloadReaders.put(streamId, payloadReader);
}
}
if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) {
long maxSearchPosition =
foundAudioTrack && foundVideoTrack
? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
: MAX_SEARCH_LENGTH;
if (input.getPosition() > maxSearchPosition) {
foundAllTracks = true;
output.endTracks();
}

View file

@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys.
return;
}
}
}
if (codecInfo == null) {
@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
@ -1209,6 +1218,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
/**
* Returns whether the device needs keys to have been loaded into the {@link DrmSession} before
* codec configuration.
*/
private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {
return "Amazon".equals(Util.MANUFACTURER)
&& ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
|| "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
}
/**
* Returns whether the decoder is known to fail when flushed.
* <p>
@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
* Returns whether the decoder is known to handle the propagation of the
* {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
* <p>
* If true is returned, the renderer will work around the issue by approximating end of stream
* Returns whether the decoder is known to handle the propagation of the {@link
* MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
*
* <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
* underlying decoder.
*
* @param name The name of the decoder.
* @param codecInfo Information about the {@link MediaCodec}.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise.
*/
private static boolean codecNeedsEosPropagationWorkaround(String name) {
return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name));
private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
String name = codecInfo.name;
return (Util.SDK_INT <= 17
&& ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
}
/**

View file

@ -0,0 +1,96 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/** Internal ID3 frame that is intended for use by the player. */
public final class InternalFrame extends Id3Frame {
public static final String ID = "----";
public final String domain;
public final String description;
public final String text;
public InternalFrame(String domain, String description, String text) {
super(ID);
this.domain = domain;
this.description = description;
this.text = text;
}
/* package */ InternalFrame(Parcel in) {
super(ID);
domain = Assertions.checkNotNull(in.readString());
description = Assertions.checkNotNull(in.readString());
text = Assertions.checkNotNull(in.readString());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
InternalFrame other = (InternalFrame) obj;
return Util.areEqual(description, other.description)
&& Util.areEqual(domain, other.domain)
&& Util.areEqual(text, other.text);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (domain != null ? domain.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
return result;
}
@Override
public String toString() {
return id + ": domain=" + domain + ", description=" + description;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(domain);
dest.writeString(text);
}
public static final Creator<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];
}
};
}

View file

@ -86,6 +86,7 @@ public abstract class DownloadService extends Service {
private DownloadManagerListener downloadManagerListener;
private int lastStartId;
private boolean startedInForeground;
private boolean taskRemoved;
/**
* Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
@ -219,12 +220,17 @@ public abstract class DownloadService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId;
taskRemoved = false;
String intentAction = null;
if (intent != null) {
intentAction = intent.getAction();
startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
}
// intentAction is null if the service is restarted or no action is specified.
if (intentAction == null) {
intentAction = ACTION_INIT;
}
logd("onStartCommand action: " + intentAction + " startId: " + startId);
switch (intentAction) {
case ACTION_INIT:
@ -260,6 +266,12 @@ public abstract class DownloadService extends Service {
return START_STICKY;
}
@Override
public void onTaskRemoved(Intent rootIntent) {
logd("onTaskRemoved rootIntent: " + rootIntent);
taskRemoved = true;
}
@Override
public void onDestroy() {
logd("onDestroy");
@ -353,8 +365,13 @@ public abstract class DownloadService extends Service {
if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready();
}
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
stopSelf();
logd("stopSelf()");
} else {
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
}
private void logd(String message) {

View file

@ -344,6 +344,14 @@ public final class AdPlaybackState {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as skipped. */
@CheckResult
public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as having a load error. */
@CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {

View file

@ -21,10 +21,10 @@ import android.text.Layout.Alignment;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder {
private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
private static final int[] COLORS = new int[] {
Color.WHITE,
Color.GREEN,
Color.BLUE,
Color.CYAN,
Color.RED,
Color.YELLOW,
Color.MAGENTA,
};
private static final int[] STYLE_COLORS =
new int[] {
Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA
};
private static final int STYLE_ITALICS = 0x07;
private static final int STYLE_UNCHANGED = 0x08;
// The default number of rows to display in roll-up captions mode.
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder {
// A midrow control code advances the cursor.
currentCueBuilder.append(' ');
// cc2 - 0|0|1|0|ATRBT|U
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle
boolean isUnderlined = (cc2 & 0x01) == 0x01;
currentCueBuilder.setUnderline(isUnderlined);
int attribute = (cc2 >> 1) & 0x0F;
if (attribute == 0x07) {
currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
} else {
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
}
// cc2 - 0|0|1|0|STYLE|U
boolean underline = (cc2 & 0x01) == 0x01;
int style = (cc2 >> 1) & 0x07;
currentCueBuilder.setStyle(style, underline);
}
private void handlePreambleAddressCode(byte cc1, byte cc2) {
@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder {
currentCueBuilder.setRow(row);
}
if ((cc2 & 0x01) == 0x01) {
currentCueBuilder.setPreambleStyle(new UnderlineSpan());
}
// cc2 - 0|1|N|0|STYLE|U
// cc2 - 0|1|N|1|CURSR|U
int attribute = cc2 >> 1 & 0x0F;
if (attribute <= 0x07) {
if (attribute == 0x07) {
currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE));
} else {
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute]));
}
} else {
currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]);
boolean isCursor = (cc2 & 0x10) == 0x10;
boolean underline = (cc2 & 0x01) == 0x01;
int cursorOrStyle = (cc2 >> 1) & 0x07;
// We need to call setStyle even for the isCursor case, to update the underline bit.
// STYLE_UNCHANGED is used for this case.
currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);
if (isCursor) {
currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]);
}
}
@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder {
private static class CueBuilder {
private static final int POSITION_UNSET = -1;
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
// positions to normalized screen position.
private static final int SCREEN_CHARWIDTH = 32;
private static final int BASE_ROW = 15;
private final List<CharacterStyle> preambleStyles;
private final List<CueStyle> midrowStyles;
private final List<CueStyle> cueStyles;
private final List<SpannableString> rolledUpCaptions;
private final SpannableStringBuilder captionStringBuilder;
private final StringBuilder captionStringBuilder;
private int row;
private int indent;
private int tabOffset;
private int captionMode;
private int captionRowCount;
private int underlineStartPosition;
public CueBuilder(int captionMode, int captionRowCount) {
preambleStyles = new ArrayList<>();
midrowStyles = new ArrayList<>();
cueStyles = new ArrayList<>();
rolledUpCaptions = new ArrayList<>();
captionStringBuilder = new SpannableStringBuilder();
captionStringBuilder = new StringBuilder();
reset(captionMode);
setCaptionRowCount(captionRowCount);
}
public void reset(int captionMode) {
this.captionMode = captionMode;
preambleStyles.clear();
midrowStyles.clear();
cueStyles.clear();
rolledUpCaptions.clear();
captionStringBuilder.clear();
captionStringBuilder.setLength(0);
row = BASE_ROW;
indent = 0;
tabOffset = 0;
underlineStartPosition = POSITION_UNSET;
}
public void setCaptionRowCount(int captionRowCount) {
@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder {
}
public boolean isEmpty() {
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
return cueStyles.isEmpty()
&& rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0;
}
@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder {
int length = captionStringBuilder.length();
if (length > 0) {
captionStringBuilder.delete(length - 1, length);
// Decrement style start positions if necessary.
for (int i = cueStyles.size() - 1; i >= 0; i--) {
CueStyle style = cueStyles.get(i);
if (style.start == length) {
style.start--;
} else {
// All earlier cues must have style.start < length.
break;
}
}
}
}
@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder {
public void rollUp() {
rolledUpCaptions.add(buildSpannableString());
captionStringBuilder.clear();
preambleStyles.clear();
midrowStyles.clear();
underlineStartPosition = POSITION_UNSET;
captionStringBuilder.setLength(0);
cueStyles.clear();
int numRows = Math.min(captionRowCount, row);
while (rolledUpCaptions.size() >= numRows) {
rolledUpCaptions.remove(0);
@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder {
tabOffset = tabs;
}
public void setPreambleStyle(CharacterStyle style) {
preambleStyles.add(style);
}
public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
}
public void setUnderline(boolean enabled) {
if (enabled) {
underlineStartPosition = captionStringBuilder.length();
} else if (underlineStartPosition != POSITION_UNSET) {
// underline spans won't overlap, so it's safe to modify the builder directly with them
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
underlineStartPosition = POSITION_UNSET;
}
public void setStyle(int style, boolean underline) {
cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));
}
public void append(char text) {
@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder {
}
public SpannableString buildSpannableString() {
int length = captionStringBuilder.length();
SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);
int length = builder.length();
// preamble styles apply to the entire cue
for (int i = 0; i < preambleStyles.size(); i++) {
captionStringBuilder.setSpan(preambleStyles.get(i), 0, length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
int underlineStartPosition = C.INDEX_UNSET;
int italicStartPosition = C.INDEX_UNSET;
int colorStartPosition = 0;
int color = Color.WHITE;
boolean nextItalic = false;
int nextColor = Color.WHITE;
for (int i = 0; i < cueStyles.size(); i++) {
CueStyle cueStyle = cueStyles.get(i);
boolean underline = cueStyle.underline;
int style = cueStyle.style;
if (style != STYLE_UNCHANGED) {
// If the style is a color then italic is cleared.
nextItalic = style == STYLE_ITALICS;
// If the style is italic then the color is left unchanged.
nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];
}
int position = cueStyle.start;
int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;
if (position == nextPosition) {
// There are more cueStyles to process at the current position.
continue;
}
// Process changes to underline up to the current position.
if (underlineStartPosition != C.INDEX_UNSET && !underline) {
setUnderlineSpan(builder, underlineStartPosition, position);
underlineStartPosition = C.INDEX_UNSET;
} else if (underlineStartPosition == C.INDEX_UNSET && underline) {
underlineStartPosition = position;
}
// Process changes to italic up to the current position.
if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {
setItalicSpan(builder, italicStartPosition, position);
italicStartPosition = C.INDEX_UNSET;
} else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {
italicStartPosition = position;
}
// Process changes to color up to the current position.
if (nextColor != color) {
setColorSpan(builder, colorStartPosition, position, color);
color = nextColor;
colorStartPosition = position;
}
}
// midrow styles only apply to part of the cue, and after preamble styles
for (int i = 0; i < midrowStyles.size(); i++) {
CueStyle cueStyle = midrowStyles.get(i);
int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement)
? midrowStyles.get(i + cueStyle.nextStyleIncrement).start
: length;
captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// Add any final spans.
if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {
setUnderlineSpan(builder, underlineStartPosition, length);
}
if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {
setItalicSpan(builder, italicStartPosition, length);
}
if (colorStartPosition != length) {
setColorSpan(builder, colorStartPosition, length, color);
}
// special case for midrow underlines that went to the end of the cue
if (underlineStartPosition != POSITION_UNSET) {
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return new SpannableString(captionStringBuilder);
return new SpannableString(builder);
}
public Cue build() {
@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder {
return captionStringBuilder.toString();
}
private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {
builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static void setColorSpan(
SpannableStringBuilder builder, int start, int end, int color) {
if (color == Color.WHITE) {
// White is treated as the default color (i.e. no span is attached).
return;
}
builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static class CueStyle {
public final CharacterStyle style;
public final int start;
public final int nextStyleIncrement;
public final int style;
public final boolean underline;
public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) {
public int start;
public CueStyle(int style, boolean underline, int start) {
this.style = style;
this.underline = underline;
this.start = start;
this.nextStyleIncrement = nextStyleIncrement;
}
}

View file

@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class DefaultTrackSelector extends MappingTrackSelector {
/**
* A builder for {@link Parameters}.
* A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of
* the parameters that can be configured using this builder.
*/
public static final class ParametersBuilder {
@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private boolean viewportOrientationMayChange;
private int tunnelingAudioSessionId;
/**
* Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}.
*/
/** Creates a builder with default initial values. */
public ParametersBuilder() {
this(Parameters.DEFAULT);
}
@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
* Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from
* {@link Util#getPhysicalDisplaySize(Context)}.
* Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
* obtained from {@link Util#getPhysicalDisplaySize(Context)}.
*
* @param context The context to obtain the viewport size from.
* @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}.
* @param context Any context.
* @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder.
*/
public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context,
boolean viewportOrientationMayChange) {
public ParametersBuilder setViewportSizeToPhysicalDisplaySize(
Context context, boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen.
Point viewportSize = Util.getPhysicalDisplaySize(context);
return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
* See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and
* {@link Parameters#viewportOrientationMayChange}.
* See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link
* Parameters#viewportOrientationMayChange}.
*
* @param viewportWidth See {@link Parameters#viewportWidth}.
* @param viewportHeight See {@link Parameters#viewportHeight}.
* @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder.
*/
public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight,
boolean viewportOrientationMayChange) {
public ParametersBuilder setViewportSize(
int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
this.viewportOrientationMayChange = viewportOrientationMayChange;
@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
* Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in
* tunneling mode. Session ids can be generated using {@link
* See {@link Parameters#tunnelingAudioSessionId}.
*
* <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#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
* supported by the audio and video renderers for the selected tracks.
@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** Constraint parameters for {@link DefaultTrackSelector}. */
public static final class Parameters implements Parcelable {
/**
* An instance with default values:
*
* <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>
*/
/** An instance with default values. */
public static final Parameters DEFAULT = new Parameters();
// Per renderer overrides.
@ -568,105 +554,131 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Audio
/**
* The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag.
* {@code null} selects the default track, or the first track if there's no default.
* The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null}
* selects the default track, or the first track if there's no default. The default value is
* {@code null}.
*/
public final @Nullable String preferredAudioLanguage;
// Text
/**
* The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the
* default track if there is one, or no track otherwise.
* default track if there is one, or no track otherwise. The default value is {@code null}.
*/
public final @Nullable String preferredTextLanguage;
/**
* Whether a text track with undetermined language should be selected if no track with
* {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset.
* Whether a text track with undetermined language should be selected if no track with {@link
* #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The
* default value is {@code false}.
*/
public final boolean selectUndeterminedTextLanguage;
/**
* Bitmask of selection flags that are disabled for text track selections. See {@link
* C.SelectionFlags}.
* C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).
*/
public final int disabledTextTrackSelectionFlags;
// Video
/**
* Maximum allowed video width.
* Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no
* constraint).
*
* <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;
/**
* 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;
/**
* Maximum video bitrate.
* Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint).
*/
public final int maxVideoBitrate;
/**
* Whether to exceed video constraints when no selection can be made otherwise.
* Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link
* #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is
* {@code true}.
*/
public final boolean exceedVideoConstraintsIfNecessary;
/**
* Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that
* only tracks suitable for the viewport are selected.
* Viewport width in pixels. Constrains video track selections for adaptive content so that only
* tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE}
* (i.e. no constraint).
*/
public final int viewportWidth;
/**
* Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that
* only tracks suitable for the viewport are selected.
* Viewport height in pixels. Constrains video track selections for adaptive content so that
* only tracks suitable for the viewport are selected. The default value is {@link
* Integer#MAX_VALUE} (i.e. no constraint).
*/
public final int viewportHeight;
/**
* Whether the viewport orientation may change during playback. Constrains video tracks
* selections for adaptive playbacks so that only tracks suitable for the viewport are selected.
* Whether the viewport orientation may change during playback. Constrains video track
* selections for adaptive content so that only tracks suitable for the viewport are selected.
* The default value is {@code true}.
*/
public final boolean viewportOrientationMayChange;
// General
/**
* Whether to force selection of the single lowest bitrate audio and video tracks that comply
* with all other constraints.
* with all other constraints. The default value is {@code false}.
*/
public final boolean forceLowestBitrate;
/**
* Whether to allow adaptive selections containing mixed mime types.
* Whether to allow adaptive selections containing mixed mime types. The default value is {@code
* false}.
*/
public final boolean allowMixedMimeAdaptiveness;
/**
* Whether to allow adaptive selections where adaptation may not be completely seamless.
* Whether to allow adaptive selections where adaptation may not be completely seamless. The
* default value is {@code true}.
*/
public final boolean allowNonSeamlessAdaptiveness;
/**
* Whether to exceed renderer capabilities when no selection can be made otherwise.
*
* <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;
/**
* The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
* is not to be enabled.
* is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is
* disabled).
*/
public final int tunnelingAudioSessionId;
private Parameters() {
this(
new SparseArray<Map<TrackGroupArray, SelectionOverride>>(),
new SparseBooleanArray(),
null,
null,
false,
0,
false,
false,
true,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
true,
true,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
true,
C.AUDIO_SESSION_ID_UNSET);
/* selectionOverrides= */ new SparseArray<Map<TrackGroupArray,SelectionOverride>>(),
/* rendererDisabledFlags= */ new SparseBooleanArray(),
/* preferredAudioLanguage= */ null,
/* preferredTextLanguage= */ null,
/* selectUndeterminedTextLanguage= */ false,
/* disabledTextTrackSelectionFlags= */ 0,
/* forceLowestBitrate= */ false,
/* allowMixedMimeAdaptiveness= */ false,
/* allowNonSeamlessAdaptiveness= */ true,
/* maxVideoWidth= */ Integer.MAX_VALUE,
/* maxVideoHeight= */ Integer.MAX_VALUE,
/* maxVideoBitrate= */ Integer.MAX_VALUE,
/* exceedVideoConstraintsIfNecessary= */ true,
/* exceedRendererCapabilitiesIfNecessary= */ true,
/* viewportWidth= */ Integer.MAX_VALUE,
/* viewportHeight= */ Integer.MAX_VALUE,
/* viewportOrientationMayChange= */ true,
/* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET);
}
/* package */ Parameters(

View file

@ -30,7 +30,8 @@ public final class TimestampAdjuster {
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;
@ -38,13 +39,13 @@ public final class TimestampAdjuster {
private long timestampOffsetUs;
// 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)}.
*/
public TimestampAdjuster(long firstSampleTimestampUs) {
lastSampleTimestamp = C.TIME_UNSET;
lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs);
}
@ -56,30 +57,24 @@ public final class TimestampAdjuster {
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
*/
public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET);
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
this.firstSampleTimestampUs = firstSampleTimestampUs;
}
/**
* Returns the first adjusted sample timestamp in microseconds.
*
* @return The first adjusted sample timestamp in microseconds.
*/
/** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
public long getFirstSampleTimestampUs() {
return firstSampleTimestampUs;
}
/**
* Returns the last adjusted timestamp. If no timestamp has been adjusted, returns
* {@code firstSampleTimestampUs} as provided to the constructor. 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.
* Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link
* #adjustSampleTimestamp} has not been called, returns the result of calling {@link
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* C#TIME_UNSET}.
*/
public long getLastAdjustedTimestampUs() {
return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp
return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
}
@ -93,44 +88,47 @@ public final class TimestampAdjuster {
* be offset.
*/
public long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET ? 0
: lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
return firstSampleTimestampUs == DO_NOT_OFFSET
? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
}
/**
* Resets the instance to its initial state.
*/
public void reset() {
lastSampleTimestamp = C.TIME_UNSET;
lastSampleTimestampUs = C.TIME_UNSET;
}
/**
* 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.
*/
public long adjustTsTimestamp(long pts) {
if (pts == C.TIME_UNSET) {
public long adjustTsTimestamp(long pts90Khz) {
if (pts90Khz == 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),
// and we need to snap to the one closest to lastSampleTimestamp.
long lastPts = usToPts(lastSampleTimestamp);
// and we need to snap to the one closest to lastSampleTimestampUs.
long lastPts = usToPts(lastSampleTimestampUs);
long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow : ptsWrapAbove;
long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts90Khz =
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.
*/
public long adjustSampleTimestamp(long timeUs) {
@ -138,15 +136,15 @@ public final class TimestampAdjuster {
return C.TIME_UNSET;
}
// Record the adjusted PTS to adjust for wraparound next time.
if (lastSampleTimestamp != C.TIME_UNSET) {
lastSampleTimestamp = timeUs;
if (lastSampleTimestampUs != C.TIME_UNSET) {
lastSampleTimestampUs = timeUs;
} else {
if (firstSampleTimestampUs != DO_NOT_OFFSET) {
// Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs;
}
synchronized (this) {
lastSampleTimestamp = timeUs;
lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized.
notifyAll();
}
@ -160,15 +158,15 @@ public final class TimestampAdjuster {
* @throws InterruptedException If the thread was interrupted.
*/
public synchronized void waitUntilInitialized() throws InterruptedException {
while (lastSampleTimestamp == C.TIME_UNSET) {
while (lastSampleTimestampUs == C.TIME_UNSET) {
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.
*/
public static long ptsToUs(long pts) {
@ -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.
* @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) {
return (us * 90000) / C.MICROS_PER_SECOND;

View file

@ -56,8 +56,7 @@ public final class XmlPullParserUtil {
* @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 isStartTag(XmlPullParser xpp, String name)
throws XmlPullParserException {
public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
return isStartTag(xpp) && xpp.getName().equals(name);
}
@ -72,22 +71,59 @@ public final class XmlPullParserUtil {
return xpp.getEventType() == XmlPullParser.START_TAG;
}
/**
* 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.
*
* @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
* no such attribute was found.
* such attribute was found.
*/
public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount();
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 null;
}
private static String stripPrefix(String name) {
int prefixSeparatorIndex = name.indexOf(':');
return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1);
}
}

View file

@ -84,6 +84,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// pending output streams that have fewer frames than the codec latency.
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 VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
private final EventDispatcher eventDispatcher;
@ -459,7 +462,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width
&& newFormat.height <= codecMaxValues.height
&& getMaxInputSize(newFormat) <= codecMaxValues.inputSize) {
&& getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) {
return oldFormat.initializationDataEquals(newFormat)
? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION
: KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION;
@ -981,7 +984,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
throws DecoderQueryException {
int maxWidth = format.width;
int maxHeight = format.height;
int maxInputSize = getMaxInputSize(format);
int maxInputSize = getMaxInputSize(codecInfo, format);
if (streamFormats.length == 1) {
// The single entry in streamFormats must correspond to the format for which the codec is
// being configured.
@ -994,7 +997,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
(streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE);
maxWidth = Math.max(maxWidth, streamFormat.width);
maxHeight = Math.max(maxHeight, streamFormat.height);
maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat));
maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat));
}
}
if (haveUnknownDimensions) {
@ -1004,7 +1007,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maxWidth = Math.max(maxWidth, codecMaxSize.x);
maxHeight = Math.max(maxHeight, codecMaxSize.y);
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);
}
}
@ -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.
* @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not
* be determined.
*/
private static int getMaxInputSize(Format format) {
private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) {
if (format.maxInputSize != Format.NO_VALUE) {
// 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.
@ -1072,20 +1078,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} else {
// Calculated maximum input sizes are overestimates, so it's not necessary to add the size of
// 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 width The width 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
* 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) {
// We can't infer a maximum input size without video dimensions.
return Format.NO_VALUE;
@ -1101,9 +1109,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL)) {
// The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video
// maximum input size, so use the default value.
if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
|| ("Amazon".equals(Util.MANUFACTURER)
&& ("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;
}
// Round up width/height to an integer number of macroblocks.
@ -1163,14 +1174,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
}
/**
* Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)}
* incorrectly.
* <p>
* If true is returned then we fall back to releasing and re-instantiating the codec instead.
/*
* 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.
*/
private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
// Work around https://github.com/google/ExoPlayer/issues/3236,
/**
* Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)}
* incorrectly.
*
* <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.
*/
protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
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/3439,
// https://github.com/google/ExoPlayer/issues/3724,
@ -1179,28 +1212,150 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104,
// https://github.com/google/ExoPlayer/issues/4134,
// https://github.com/google/ExoPlayer/issues/4315.
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|| "mido".equals(Util.DEVICE) // Redmi Note 4
|| "santoni".equals(Util.DEVICE)) // Redmi 4X
&& "OMX.qcom.video.decoder.avc".equals(name))
|| (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV
|| "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015
|| "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB
|| Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|| "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
|| "woods_f".equals(Util.MODEL) // Moto E (4)
|| "watson".equals(Util.DEVICE)) // Moto C
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II
&& "OMX.k3.video.decoder.avc".equals(name))
|| (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite
&& "OMX.IMG.MSVDX.Decoder.AVC".equals(name));
// https://github.com/google/ExoPlayer/issues/4315,
// https://github.com/google/ExoPlayer/issues/4419,
// https://github.com/google/ExoPlayer/issues/4460,
// https://github.com/google/ExoPlayer/issues/4468.
synchronized (MediaCodecVideoRenderer.class) {
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
switch (Util.DEVICE) {
case "1601":
case "1713":
case "1714":
case "A10-70F":
case "A1601":
case "A2016a40":
case "A7000-a":
case "A7000plus":
case "A7010a48":
case "A7020a48":
case "AquaPowerM":
case "Aura_Note_2":
case "BLACK-1X":
case "BRAVIA_ATV2":
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 {

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.EventListener;
@ -1980,6 +1981,44 @@ public final class ExoPlayerTest {
.inOrder();
}
@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 {
// We add two listeners to the player. The first stops the player as soon as it's ready and both
@ -2040,7 +2079,7 @@ public final class ExoPlayerTest {
final EventListener eventListener =
new DefaultEventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.isEmpty()) {
playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);
}

View file

@ -89,6 +89,19 @@ public final class AdPlaybackStateTest {
assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);
}
@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() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.dash;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -62,7 +61,6 @@ import java.util.List;
/* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory;
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final long elapsedRealtimeOffset;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator;
@ -73,6 +71,7 @@ import java.util.List;
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
trackEmsgHandlerBySampleStream;
private EventDispatcher eventDispatcher;
private @Nullable Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private EventSampleStream[] eventSampleStreams;
@ -127,6 +126,13 @@ import java.util.List;
*/
public void updateManifest(DashManifest manifest, int periodIndex) {
this.manifest = manifest;
if (this.periodIndex != periodIndex) {
eventDispatcher =
eventDispatcher.withParameters(
/* windowIndex= */ 0,
eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex),
manifest.getPeriod(periodIndex).startMs);
}
this.periodIndex = periodIndex;
playerEmsgHandler.updateManifest(manifest);
if (sampleStreams != null) {
@ -139,7 +145,10 @@ import java.util.List;
for (EventSampleStream eventSampleStream : eventSampleStreams) {
for (EventStream eventStream : eventStreams) {
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;
}
}
@ -186,126 +195,34 @@ import java.util.List;
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
SparseArray<ChunkSampleStream<DashChunkSource>> primarySampleStreams = new SparseArray<>();
List<EventSampleStream> eventSampleStreamList = new ArrayList<>();
int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections);
releaseDisabledStreams(selections, mayRetainStreamFlags, streams);
releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex);
selectNewStreams(
selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex);
selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs, primarySampleStreams);
selectEventSampleStreams(selections, mayRetainStreamFlags, streams,
streamResetFlags, eventSampleStreamList);
selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs, primarySampleStreams);
sampleStreams = newSampleStreamArray(primarySampleStreams.size());
for (int i = 0; i < sampleStreams.length; i++) {
sampleStreams[i] = primarySampleStreams.valueAt(i);
ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamList = new ArrayList<>();
ArrayList<EventSampleStream> eventSampleStreamList = new ArrayList<>();
for (SampleStream sampleStream : streams) {
if (sampleStream instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream =
(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()];
eventSampleStreamList.toArray(eventSampleStreams);
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
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
public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
@ -372,6 +289,124 @@ import java.util.List;
// 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(
List<AdaptationSet> adaptationSets, List<EventStream> eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
@ -624,12 +659,6 @@ import java.util.List;
return new ChunkSampleStream[length];
}
private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) {
if (sampleStream instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) sampleStream).release();
}
}
private static final class TrackGroupInfo {
@Retention(RetentionPolicy.SOURCE)

View file

@ -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.ads.AdsMediaSource;
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.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource {
long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false;
boolean seenEmptyIndex = false;
boolean haveAudioVideoAdaptationSets = false;
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) {
return new PeriodSeekInfo(true, 0, durationUs);
}

View file

@ -36,37 +36,53 @@ import java.io.IOException;
private final EventMessageEncoder eventMessageEncoder;
private long[] eventTimesUs;
private boolean eventStreamUpdatable;
private boolean eventStreamAppendable;
private EventStream eventStream;
private boolean isFormatSentDownstream;
private int currentIndex;
private long pendingSeekPositionUs;
EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) {
public EventSampleStream(
EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) {
this.upstreamFormat = upstreamFormat;
this.eventStream = eventStream;
eventMessageEncoder = new EventMessageEncoder();
pendingSeekPositionUs = C.TIME_UNSET;
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];
this.eventStreamUpdatable = eventStreamUpdatable;
this.eventStreamAppendable = eventStreamAppendable;
this.eventStream = eventStream;
this.eventTimesUs = eventStream.presentationTimesUs;
if (pendingSeekPositionUs != C.TIME_UNSET) {
seekToUs(pendingSeekPositionUs);
} 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
@ -88,7 +104,7 @@ import java.io.IOException;
return C.RESULT_FORMAT_READ;
}
if (currentIndex == eventTimesUs.length) {
if (!eventStreamUpdatable) {
if (!eventStreamAppendable) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else {
@ -118,15 +134,4 @@ import java.io.IOException;
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;
}
}

View file

@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler
protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeType = null;
String licenseServerUrl = null;
byte[] data = null;
UUID uuid = null;
boolean requiresSecureDecoder = false;
@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler
switch (Util.toLowerInvariant(schemeIdUri)) {
case "urn:mpeg:dash:mp4protection:2011":
schemeType = xpp.getAttributeValue(null, "value");
String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID");
String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID");
if (!TextUtils.isEmpty(defaultKid)
&& !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) {
String[] defaultKidStrings = defaultKid.split("\\s+");
@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler
do {
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");
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
} 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.
data = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(data);
@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler
}
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
SchemeData schemeData = uuid != null
? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null;
SchemeData schemeData =
uuid != null
? new SchemeData(
uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
: null;
return Pair.create(schemeType, schemeData);
}

View file

@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public void releaseSourceInternal() {
if (playlistTracker != null) {
playlistTracker.release();
playlistTracker.stop();
}
}

View file

@ -136,6 +136,7 @@ import java.util.Arrays;
// Accessed only by the loading thread.
private boolean tracksEnded;
private long sampleOffsetUs;
private int chunkUid;
/**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
@ -650,6 +651,7 @@ import java.util.Arrays;
audioSampleQueueMappingDone = false;
videoSampleQueueMappingDone = false;
}
this.chunkUid = chunkUid;
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(chunkUid);
}
@ -704,6 +706,7 @@ import java.util.Arrays;
}
}
SampleQueue trackOutput = new SampleQueue(allocator);
trackOutput.sourceId(chunkUid);
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);

View file

@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker
}
@Override
public void release() {
public void stop() {
primaryHlsUrl = null;
primaryUrlSnapshot = null;
masterPlaylist = null;

View file

@ -100,8 +100,8 @@ public interface HlsPlaylistTracker {
/**
* Starts the playlist tracker.
*
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link
* #release()} call.
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
* call.
*
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist.
@ -111,8 +111,12 @@ public interface HlsPlaylistTracker {
void start(
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.

View file

@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class Aes128DataSourceTest {
@Ignore
@Test
public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = new UpstreamDataSource();
@ -44,6 +47,7 @@ public class Aes128DataSourceTest {
assertThat(upstream.opened).isFalse();
}
@Ignore
@Test
public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream =

View file

@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
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 progressBar;
private final Rect bufferedBar;
@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Paint adMarkerPaint;
private final Paint playedAdMarkerPaint;
private final Paint scrubberPaint;
private final Drawable scrubberDrawable;
private final @Nullable Drawable scrubberDrawable;
private final int barHeight;
private final int touchTargetHeight;
private final int adMarkerWidth;
@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Formatter formatter;
private final Runnable stopScrubbingRunnable;
private final CopyOnWriteArraySet<OnScrubListener> listeners;
private final int[] locationOnScreen;
private final Point touchPosition;
private int keyCountIncrement;
private long keyTimeIncrement;
private int lastCoarseScrubXPosition;
private int[] locationOnScreen;
private Point touchPosition;
private boolean scrubbing;
private long scrubPosition;
@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar {
private long position;
private long bufferedPosition;
private int adGroupCount;
private long[] adGroupTimesMs;
private boolean[] playedAdGroups;
private @Nullable long[] adGroupTimesMs;
private @Nullable boolean[] playedAdGroups;
/**
* Creates a new time bar.
*/
/** Creates a new time bar. */
public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs);
seekBounds = new Rect();
@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberPaint = new Paint();
scrubberPaint.setAntiAlias(true);
listeners = new CopyOnWriteArraySet<>();
locationOnScreen = new int[2];
touchPosition = new Point();
// Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources();
@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText());
}
event.setClassName(DefaultTimeBar.class.getName());
event.setClassName(ACCESSIBILITY_CLASS_NAME);
}
@TargetApi(21)
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(DefaultTimeBar.class.getCanonicalName());
info.setClassName(ACCESSIBILITY_CLASS_NAME);
info.setContentDescription(getProgressText());
if (duration <= 0) {
return;
@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar {
@TargetApi(16)
@Override
public boolean performAccessibilityAction(int action, Bundle args) {
public boolean performAccessibilityAction(int action, @Nullable Bundle args) {
if (super.performAccessibilityAction(action, args)) {
return true;
}
@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar {
}
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
if (locationOnScreen == null) {
locationOnScreen = new int[2];
touchPosition = new Point();
}
getLocationOnScreen(locationOnScreen);
touchPosition.set(
((int) motionEvent.getRawX()) - locationOnScreen[0],
@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar {
if (scrubberBar.width() > 0) {
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;
for (int i = 0; i < adGroupCount; i++) {
long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);

View file

@ -55,10 +55,18 @@ public final class DownloadNotificationUtil {
int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false;
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;
}
if (taskState.action.isRemoveAction) {
haveRemoveTasks = true;
continue;
}
haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage;
@ -67,18 +75,20 @@ public final class DownloadNotificationUtil {
downloadTaskCount++;
}
boolean haveDownloadTasks = downloadTaskCount > 0;
int titleStringId =
haveDownloadTasks
? 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 =
newNotificationBuilder(
context, smallIcon, channelId, contentIntent, message, titleStringId);
int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0;
boolean indeterminate =
!haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes);
int progress = 0;
boolean indeterminate = true;
if (haveDownloadTasks) {
progress = (int) (totalPercentage / downloadTaskCount);
indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
}
notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
notificationBuilder.setOngoing(true);
notificationBuilder.setShowWhen(false);

View file

@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout {
@Override
public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
updateNavigation();
updateTimeBarMode();
updateProgress();

View file

@ -949,7 +949,7 @@ public class PlayerNotificationManager {
}
@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) {
return;
}

View file

@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout {
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.
*

View file

@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util;
private int edgeColor;
@CaptionStyleCompat.EdgeType
private int edgeType;
private float textSizePx;
private float defaultTextSizePx;
private float cueTextSizePx;
private float bottomPaddingFraction;
private int parentLeft;
private int parentTop;
@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util;
/**
* 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
* which the same parameters are passed.
*
@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util;
* @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font
* sizes embedded within the cue should be applied. Otherwise, it is ignored.
* @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
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw.
@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util;
* @param cueBoxRight The right position of the enclosing cue box.
* @param cueBoxBottom The bottom position of the enclosing cue box.
*/
public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes,
CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas,
int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) {
public void draw(
Cue cue,
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;
int windowColor = Color.BLACK;
if (isTextCue) {
@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util;
&& this.edgeType == style.edgeType
&& this.edgeColor == style.edgeColor
&& Util.areEqual(this.textPaint.getTypeface(), style.typeface)
&& this.textSizePx == textSizePx
&& this.defaultTextSizePx == defaultTextSizePx
&& this.cueTextSizePx == cueTextSizePx
&& this.bottomPaddingFraction == bottomPaddingFraction
&& this.parentLeft == cueBoxLeft
&& this.parentTop == cueBoxTop
@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util;
this.edgeType = style.edgeType;
this.edgeColor = style.edgeColor;
this.textPaint.setTypeface(style.typeface);
this.textSizePx = textSizePx;
this.defaultTextSizePx = defaultTextSizePx;
this.cueTextSizePx = cueTextSizePx;
this.bottomPaddingFraction = bottomPaddingFraction;
this.parentLeft = cueBoxLeft;
this.parentTop = cueBoxTop;
@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util;
int parentWidth = parentRight - parentLeft;
int parentHeight = parentBottom - parentTop;
textPaint.setTextSize(textSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
textPaint.setTextSize(defaultTextSizePx);
int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) {
@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util;
return;
}
CharSequence cueText = this.cueText;
// Remove embedded styling or font size if requested.
CharSequence cueText;
if (applyEmbeddedFontSizes && applyEmbeddedStyles) {
cueText = this.cueText;
} else if (!applyEmbeddedStyles) {
cueText = this.cueText.toString(); // Equivalent to erasing all spans.
} else {
SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);
if (!applyEmbeddedStyles) {
cueText = cueText.toString(); // Equivalent to erasing all spans.
} else if (!applyEmbeddedFontSizes) {
SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
int cueLength = newCueText.length();
AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);
RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);
@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util;
newCueText.removeSpan(relSpan);
}
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;

View file

@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput {
for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i);
float textSizePx =
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
textSizePx,
defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction,
canvas,
left,
@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput {
}
}
private float resolveTextSizeForCue(
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx;
return 0;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
return Math.max(defaultCueTextSizePx, 0);
}
private float resolveTextSize(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 146 B

View file

@ -575,7 +575,9 @@ public abstract class Action {
new Player.DefaultEventListener() {
@Override
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)) {
player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler);

View file

@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
// Player.EventListener
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
timelines.add(timeline);
manifests.add(manifest);
timelineChangeReasons.add(reason);