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