Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
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>
|
||||||
198
RELEASENOTES.md
|
|
@ -2,6 +2,197 @@
|
||||||
|
|
||||||
### dev-v2 (not yet released) ###
|
### dev-v2 (not yet released) ###
|
||||||
|
|
||||||
|
* Add a flag to opt-in to automatic audio focus handling via
|
||||||
|
`SimpleExoPlayer.setAudioAttributes`.
|
||||||
|
* Distribute Cronet extension via jCenter.
|
||||||
|
* Set compileSdkVersion and targetSdkVersion to 28.
|
||||||
|
* Add `AudioListener` for listening to changes in audio configuration during
|
||||||
|
playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
|
||||||
|
* Improved seeking support:
|
||||||
|
* Support seeking in MPEG-TS
|
||||||
|
([#966](https://github.com/google/ExoPlayer/issues/966)).
|
||||||
|
* Support seeking in MPEG-PS
|
||||||
|
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
||||||
|
* Support approximate seeking in ADTS using a constant bitrate assumption
|
||||||
|
([#4548](https://github.com/google/ExoPlayer/issues/4548)). Note that the
|
||||||
|
`FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to
|
||||||
|
enable this functionality.
|
||||||
|
* Support approximate seeking in AMR using a constant bitrate assumption.
|
||||||
|
Note that the `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the
|
||||||
|
extractor to enable this functionality.
|
||||||
|
* Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable
|
||||||
|
approximate seeking using a constant bitrate assumption for all extractors
|
||||||
|
that support it.
|
||||||
|
* MPEG-TS: Support CEA-608/708 in H262
|
||||||
|
([#2565](https://github.com/google/ExoPlayer/issues/2565)).
|
||||||
|
* MediaSession extension: Allow apps to set custom errors.
|
||||||
|
* Audio:
|
||||||
|
* Add support for mu-law and A-law PCM with the ffmpeg extension
|
||||||
|
([#4360](https://github.com/google/ExoPlayer/issues/4360)).
|
||||||
|
* Increase `AudioTrack` buffer sizes to the theoretical maximum required for
|
||||||
|
each encoding for passthrough playbacks
|
||||||
|
([#3803](https://github.com/google/ExoPlayer/issues/3803)).
|
||||||
|
* Add support for attaching auxiliary audio effects to the `AudioTrack`.
|
||||||
|
* Add support for seamless adaptation while playing xHE-AAC streams.
|
||||||
|
* Video:
|
||||||
|
* Add callback to `VideoListener` to notify of surface size changes.
|
||||||
|
* Scale up the initial video decoder maximum input size so playlist item
|
||||||
|
transitions with small increases in maximum sample size don't require
|
||||||
|
reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)).
|
||||||
|
* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
|
||||||
|
creating a `CacheDataSource`.
|
||||||
|
* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend
|
||||||
|
on ExoPlayer via its source code rather than an AAR may need to add
|
||||||
|
`compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
|
||||||
|
gradle settings to ensure bytecode compatibility.
|
||||||
|
* ConcatenatingMediaSource:
|
||||||
|
* Add support for lazy preparation of playlist media sources
|
||||||
|
([#3972](https://github.com/google/ExoPlayer/issues/3972)).
|
||||||
|
* Add support for range removal with `removeMediaSourceRange` methods.
|
||||||
|
* `BandwidthMeter` management:
|
||||||
|
* Pass `BandwidthMeter` directly to `ExoPlayerFactory` instead of
|
||||||
|
`TrackSelection.Factory` and `DataSource.Factory`. May also be omitted to
|
||||||
|
use the default bandwidth meter automatically. This change only works
|
||||||
|
correctly if the following changes are adopted for custom `BandwidthMeter`s,
|
||||||
|
`TrackSelection`s, `MediaSource`s and `DataSource`s.
|
||||||
|
* Pass `BandwidthMeter` to `TrackSelection.Factory` which should be used to
|
||||||
|
obtain bandwidth estimates.
|
||||||
|
* Add method to `BandwidthMeter` to return the `TransferListener` used to
|
||||||
|
gather bandwidth information. Also add methods to add and remove event
|
||||||
|
listeners.
|
||||||
|
* Pass `TransferListener` to `MediaSource`s to listen to media data transfers.
|
||||||
|
* Add method to `DataSource` to add `TransferListener`s. Custom `DataSource`s
|
||||||
|
directly reading data should implement `BaseDataSource` to handle the
|
||||||
|
registration correctly. Custom `DataSource`'s forwarding to other sources
|
||||||
|
should forward all calls to `addTransferListener`.
|
||||||
|
* Extend `TransferListener` with additional callback parameters.
|
||||||
|
* Error handling:
|
||||||
|
* Allow configuration of the Loader retry delay
|
||||||
|
([#3370](https://github.com/google/ExoPlayer/issues/3370)).
|
||||||
|
* HLS:
|
||||||
|
* Add support for PlayReady.
|
||||||
|
* Add support for alternative EXT-X-KEY tags.
|
||||||
|
* Set the bitrate on primary track sample formats
|
||||||
|
([#3297](https://github.com/google/ExoPlayer/issues/3297)).
|
||||||
|
* Pass HTTP response headers to `HlsExtractorFactory.createExtractor`.
|
||||||
|
* Add support for EXT-X-INDEPENDENT-SEGMENTS in the master playlist.
|
||||||
|
* Support load error handling customization
|
||||||
|
([#2981](https://github.com/google/ExoPlayer/issues/2981)).
|
||||||
|
* Fix bug when reporting buffered position for multi-period windows and add
|
||||||
|
two additional convenience methods `Player.getTotalBufferedDuration` and
|
||||||
|
`Player.getContentBufferedDuration`
|
||||||
|
([#4023](https://github.com/google/ExoPlayer/issues/4023)).
|
||||||
|
* MediaSession extension:
|
||||||
|
* Allow apps to set custom metadata with a MediaMetadataProvider
|
||||||
|
([#3497](https://github.com/google/ExoPlayer/issues/3497)).
|
||||||
|
* Improved performance when playing high frame-rate content, and when playing
|
||||||
|
at greater than 1x speed
|
||||||
|
([#2777](https://github.com/google/ExoPlayer/issues/2777)).
|
||||||
|
* Allow setting the `Looper`, which is used to access the player, in
|
||||||
|
`ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
|
||||||
|
* Use default Deserializers if non given to DownloadManager.
|
||||||
|
* Add monoscopic 360 surface type to PlayerView.
|
||||||
|
* Deprecate `Player.DefaultEventListener` as selective listener overrides can
|
||||||
|
be directly made with the `Player.EventListener` interface.
|
||||||
|
* Deprecate `DefaultAnalyticsListener` as selective listener overrides can be
|
||||||
|
directly made with the `AnalyticsListener` interface.
|
||||||
|
* Add uri field to `LoadEventInfo` in `MediaSourceEventListener` or
|
||||||
|
`AnalyticsListener` callbacks. This uri is the redirected uri if redirection
|
||||||
|
occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).
|
||||||
|
* Allow `MediaCodecSelector`s to return multiple compatible decoders for
|
||||||
|
`MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that
|
||||||
|
falls back to less preferred decoders like `MediaCodec.createDecoderByType`
|
||||||
|
([#273](https://github.com/google/ExoPlayer/issues/273)).
|
||||||
|
* Fix where transitions to clipped media sources happened too early
|
||||||
|
([#4583](https://github.com/google/ExoPlayer/issues/4583)).
|
||||||
|
* Add `DataSpec.httpMethod` and update `HttpDataSource` implementations to
|
||||||
|
support HTTP HEAD method. Previously, only GET and POST were supported.
|
||||||
|
* Add option to show buffering view when playWhenReady is false
|
||||||
|
([#4304](https://github.com/google/ExoPlayer/issues/4304)).
|
||||||
|
* Allow any `Drawable` to be used as `PlayerView` default artwork.
|
||||||
|
|
||||||
|
### 2.8.4 ###
|
||||||
|
|
||||||
|
* IMA: Improve handling of consecutive empty ad groups
|
||||||
|
([#4030](https://github.com/google/ExoPlayer/issues/4030)),
|
||||||
|
([#4280](https://github.com/google/ExoPlayer/issues/4280)).
|
||||||
|
|
||||||
|
### 2.8.3 ###
|
||||||
|
|
||||||
|
* IMA:
|
||||||
|
* Fix behavior when creating/releasing the player then releasing
|
||||||
|
`ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
|
||||||
|
* Add support for setting slots for companion ads.
|
||||||
|
* Captions:
|
||||||
|
* TTML: Fix an issue with TTML using font size as % of cell resolution that
|
||||||
|
makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly.
|
||||||
|
([#4491](https://github.com/google/ExoPlayer/issues/4491)).
|
||||||
|
* CEA-608: Improve handling of embedded styles
|
||||||
|
([#4321](https://github.com/google/ExoPlayer/issues/4321)).
|
||||||
|
* DASH:
|
||||||
|
* Exclude text streams from duration calculations
|
||||||
|
([#4029](https://github.com/google/ExoPlayer/issues/4029)).
|
||||||
|
* Fix freezing when playing multi-period manifests with `EventStream`s
|
||||||
|
([#4492](https://github.com/google/ExoPlayer/issues/4492)).
|
||||||
|
* DRM: Allow DrmInitData to carry a license server URL
|
||||||
|
([#3393](https://github.com/google/ExoPlayer/issues/3393)).
|
||||||
|
* MPEG-TS: Fix bug preventing SCTE-35 cues from being output
|
||||||
|
([#4573](https://github.com/google/ExoPlayer/issues/4573)).
|
||||||
|
* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using
|
||||||
|
CommentFrame to InternalFrame for frames with gapless metadata in MP4.
|
||||||
|
* Add `PlayerView.isControllerVisible`
|
||||||
|
([#4385](https://github.com/google/ExoPlayer/issues/4385)).
|
||||||
|
* Fix issue playing DRM protected streams on Asus Zenfone 2
|
||||||
|
([#4403](https://github.com/google/ExoPlayer/issues/4413)).
|
||||||
|
* Add support for multiple audio and video tracks in MPEG-PS streams
|
||||||
|
([#4406](https://github.com/google/ExoPlayer/issues/4406)).
|
||||||
|
* Add workaround for track index mismatches between trex and tkhd boxes in
|
||||||
|
fragmented MP4 files
|
||||||
|
([#4477](https://github.com/google/ExoPlayer/issues/4477)).
|
||||||
|
* Add workaround for track index mismatches between tfhd and tkhd boxes in
|
||||||
|
fragmented MP4 files
|
||||||
|
([#4083](https://github.com/google/ExoPlayer/issues/4083)).
|
||||||
|
* Ignore all MP4 edit lists if one edit list couldn't be handled
|
||||||
|
([#4348](https://github.com/google/ExoPlayer/issues/4348)).
|
||||||
|
* Fix issue when switching track selection from an embedded track to a primary
|
||||||
|
track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)).
|
||||||
|
* Fix accessibility class name for `DefaultTimeBar`
|
||||||
|
([#4611](https://github.com/google/ExoPlayer/issues/4611)).
|
||||||
|
* Improved compatibility with FireOS devices.
|
||||||
|
|
||||||
|
### 2.8.2 ###
|
||||||
|
|
||||||
|
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an
|
||||||
|
extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
|
||||||
|
* DASH: Fix playback getting stuck when playing representations that have both
|
||||||
|
sidx atoms and non-zero presentationTimeOffset values.
|
||||||
|
* HLS:
|
||||||
|
* Allow injection of custom playlist trackers.
|
||||||
|
* Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags.
|
||||||
|
* Mitigate memory leaks when `MediaSource` loads are slow to cancel
|
||||||
|
([#4249](https://github.com/google/ExoPlayer/issues/4249)).
|
||||||
|
* Fix inconsistent `Player.EventListener` invocations for recursive player state
|
||||||
|
changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)).
|
||||||
|
* Fix `MediaCodec.native_setSurface` crash on Moto C
|
||||||
|
([#4315](https://github.com/google/ExoPlayer/issues/4315)).
|
||||||
|
* Fix missing whitespace in CEA-608
|
||||||
|
([#3906](https://github.com/google/ExoPlayer/issues/3906)).
|
||||||
|
* Fix crash downloading HLS media playlists
|
||||||
|
([#4396](https://github.com/google/ExoPlayer/issues/4396)).
|
||||||
|
* Fix a bug where download cancellation was ignored
|
||||||
|
([#4403](https://github.com/google/ExoPlayer/issues/4403)).
|
||||||
|
* Set `METADATA_KEY_TITLE` on media descriptions
|
||||||
|
([#4292](https://github.com/google/ExoPlayer/issues/4292)).
|
||||||
|
* Allow apps to register custom MIME types
|
||||||
|
([#4264](https://github.com/google/ExoPlayer/issues/4264)).
|
||||||
|
|
||||||
|
### 2.8.1 ###
|
||||||
|
|
||||||
|
* HLS:
|
||||||
|
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
|
||||||
|
([#4239](https://github.com/google/ExoPlayer/issues/4239)).
|
||||||
|
* Fix playback of clipped streams starting from non-keyframe positions
|
||||||
|
([#4241](https://github.com/google/ExoPlayer/issues/4241)).
|
||||||
* OkHttp extension: Fix to correctly include response headers in thrown
|
* OkHttp extension: Fix to correctly include response headers in thrown
|
||||||
`InvalidResponseCodeException`s.
|
`InvalidResponseCodeException`s.
|
||||||
* Add possibility to cancel `PlayerMessage`s.
|
* Add possibility to cancel `PlayerMessage`s.
|
||||||
|
|
@ -19,10 +210,7 @@
|
||||||
([#4228](https://github.com/google/ExoPlayer/issues/4228)).
|
([#4228](https://github.com/google/ExoPlayer/issues/4228)).
|
||||||
* FLAC: Supports seeking for FLAC files without SEEKTABLE
|
* FLAC: Supports seeking for FLAC files without SEEKTABLE
|
||||||
([#1808](https://github.com/google/ExoPlayer/issues/1808)).
|
([#1808](https://github.com/google/ExoPlayer/issues/1808)).
|
||||||
* HLS:
|
* Captions:
|
||||||
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
|
|
||||||
([#4239](https://github.com/google/ExoPlayer/issues/4239)).
|
|
||||||
* Caption:
|
|
||||||
* TTML:
|
* TTML:
|
||||||
* Fix a styling issue when there are multiple regions displayed at the same
|
* Fix a styling issue when there are multiple regions displayed at the same
|
||||||
time that can make text size of each region much smaller than defined.
|
time that can make text size of each region much smaller than defined.
|
||||||
|
|
@ -57,7 +245,7 @@
|
||||||
periods are created, released and being read from.
|
periods are created, released and being read from.
|
||||||
* Support live stream clipping with `ClippingMediaSource`.
|
* Support live stream clipping with `ClippingMediaSource`.
|
||||||
* Allow setting tags for all media sources in their factories. The tag of the
|
* Allow setting tags for all media sources in their factories. The tag of the
|
||||||
current window can be retrieved with `ExoPlayer.getCurrentTag`.
|
current window can be retrieved with `Player.getCurrentTag`.
|
||||||
* UI components:
|
* UI components:
|
||||||
* Add support for displaying error messages and a buffering spinner in
|
* Add support for displaying error messages and a buffering spinner in
|
||||||
`PlayerView`.
|
`PlayerView`.
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ buildscript {
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.0'
|
classpath 'com.android.tools.build:gradle:3.1.4'
|
||||||
classpath 'com.novoda:bintray-release:0.8.1'
|
classpath 'com.novoda:bintray-release:0.8.1'
|
||||||
|
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3'
|
||||||
}
|
}
|
||||||
// Workaround for the following test coverage issue. Remove when fixed:
|
// Workaround for the following test coverage issue. Remove when fixed:
|
||||||
// https://code.google.com/p/android/issues/detail?id=226070
|
// https://code.google.com/p/android/issues/detail?id=226070
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,18 @@
|
||||||
// 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.0'
|
releaseVersion = '2.8.4'
|
||||||
releaseVersionCode = 2800
|
releaseVersionCode = 2804
|
||||||
// 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
|
||||||
// by the library requires API level 16 or greater.
|
// by the library requires API level 16 or greater.
|
||||||
minSdkVersion = 14
|
minSdkVersion = 14
|
||||||
targetSdkVersion = 27
|
targetSdkVersion = 28
|
||||||
compileSdkVersion = 27
|
compileSdkVersion = 28
|
||||||
buildToolsVersion = '27.0.3'
|
buildToolsVersion = '28.0.2'
|
||||||
testSupportLibraryVersion = '0.5'
|
testSupportLibraryVersion = '0.5'
|
||||||
supportLibraryVersion = '27.0.0'
|
supportLibraryVersion = '27.1.1'
|
||||||
playServicesLibraryVersion = '12.0.0'
|
|
||||||
dexmakerVersion = '1.2'
|
dexmakerVersion = '1.2'
|
||||||
mockitoVersion = '1.9.5'
|
mockitoVersion = '1.9.5'
|
||||||
junitVersion = '4.12'
|
junitVersion = '4.12'
|
||||||
|
|
@ -33,6 +32,7 @@ project.ext {
|
||||||
robolectricVersion = '3.7.1'
|
robolectricVersion = '3.7.1'
|
||||||
autoValueVersion = '1.6'
|
autoValueVersion = '1.6'
|
||||||
checkerframeworkVersion = '2.5.0'
|
checkerframeworkVersion = '2.5.0'
|
||||||
|
testRunnerVersion = '1.1.0-alpha3'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ include modulePrefix + 'extension-flac'
|
||||||
include modulePrefix + 'extension-gvr'
|
include modulePrefix + 'extension-gvr'
|
||||||
include modulePrefix + 'extension-ima'
|
include modulePrefix + 'extension-ima'
|
||||||
include modulePrefix + 'extension-cast'
|
include modulePrefix + 'extension-cast'
|
||||||
|
include modulePrefix + 'extension-cronet'
|
||||||
include modulePrefix + 'extension-mediasession'
|
include modulePrefix + 'extension-mediasession'
|
||||||
include modulePrefix + 'extension-okhttp'
|
include modulePrefix + 'extension-okhttp'
|
||||||
include modulePrefix + 'extension-opus'
|
include modulePrefix + 'extension-opus'
|
||||||
|
|
@ -51,6 +52,7 @@ project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensi
|
||||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
||||||
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
|
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||||
|
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||||
|
|
@ -58,9 +60,3 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
|
||||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||||
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
||||||
|
|
||||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
|
||||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
|
||||||
include modulePrefix + 'extension-cronet'
|
|
||||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
|
|
@ -57,3 +62,5 @@ dependencies {
|
||||||
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||||
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,15 @@ 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;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Player.DefaultEventListener;
|
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
|
import com.google.android.exoplayer2.Player.EventListener;
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
|
@ -36,14 +37,11 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import com.google.android.gms.cast.MediaInfo;
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
import com.google.android.gms.cast.MediaMetadata;
|
||||||
|
|
@ -51,11 +49,9 @@ import com.google.android.gms.cast.MediaQueueItem;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
||||||
* Manages players and an internal media queue for the ExoPlayer/Cast demo app.
|
/* package */ final class PlayerManager
|
||||||
*/
|
implements EventListener, CastPlayer.SessionAvailabilityListener {
|
||||||
/* package */ final class PlayerManager extends DefaultEventListener
|
|
||||||
implements CastPlayer.SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for changes in the media queue playback position.
|
* Listener for changes in the media queue playback position.
|
||||||
|
|
@ -70,9 +66,8 @@ import java.util.ArrayList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
|
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
private final PlayerView localPlayerView;
|
||||||
private final PlayerControlView castControlView;
|
private final PlayerControlView castControlView;
|
||||||
|
|
@ -119,9 +114,9 @@ import java.util.ArrayList;
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
||||||
exoPlayer.addListener(this);
|
exoPlayer.addListener(this);
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
|
|
||||||
|
|
@ -282,7 +277,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;
|
||||||
|
|
@ -396,13 +391,9 @@ import java.util.ArrayList;
|
||||||
Uri uri = Uri.parse(sample.uri);
|
Uri uri = Uri.parse(sample.uri);
|
||||||
switch (sample.mimeType) {
|
switch (sample.mimeType) {
|
||||||
case DemoUtil.MIME_TYPE_SS:
|
case DemoUtil.MIME_TYPE_SS:
|
||||||
return new SsMediaSource.Factory(
|
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
case DemoUtil.MIME_TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
case DemoUtil.MIME_TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
|
|
@ -51,3 +56,5 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'extension-ima')
|
implementation project(modulePrefix + 'extension-ima')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,14 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
|
@ -46,8 +42,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
||||||
|
|
||||||
private final ImaAdsLoader adsLoader;
|
private final ImaAdsLoader adsLoader;
|
||||||
private final DataSource.Factory manifestDataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
private final DataSource.Factory mediaDataSourceFactory;
|
|
||||||
|
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
private long contentPosition;
|
private long contentPosition;
|
||||||
|
|
@ -55,21 +50,14 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
public PlayerManager(Context context) {
|
public PlayerManager(Context context) {
|
||||||
String adTag = context.getString(R.string.ad_tag_url);
|
String adTag = context.getString(R.string.ad_tag_url);
|
||||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||||
manifestDataSourceFactory =
|
dataSourceFactory =
|
||||||
new DefaultDataSourceFactory(
|
new DefaultDataSourceFactory(
|
||||||
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||||
mediaDataSourceFactory =
|
|
||||||
new DefaultDataSourceFactory(
|
|
||||||
context,
|
|
||||||
Util.getUserAgent(context, context.getString(R.string.application_name)),
|
|
||||||
new DefaultBandwidthMeter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(Context context, PlayerView playerView) {
|
public void init(Context context, PlayerView playerView) {
|
||||||
// Create a default track selector.
|
// Create a default track selector.
|
||||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||||
TrackSelection.Factory videoTrackSelectionFactory =
|
|
||||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
|
||||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||||
|
|
||||||
// Create a player instance.
|
// Create a player instance.
|
||||||
|
|
@ -133,18 +121,13 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
@ContentType int type = Util.inferContentType(uri);
|
@ContentType int type = Util.inferContentType(uri);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
|
||||||
manifestDataSourceFactory)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(
|
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
|
|
@ -70,3 +75,5 @@ dependencies {
|
||||||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||||
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
package="com.google.android.exoplayer2.demo">
|
package="com.google.android.exoplayer2.demo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
|
|
@ -78,7 +79,7 @@
|
||||||
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
|
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,22 @@
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Google Glass (MP4,H264)",
|
"name": "Google Glass (MP4,H264)",
|
||||||
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
||||||
"extension": "mpd"
|
"extension": "mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play (MP4,H264)",
|
"name": "Google Play (MP4,H264)",
|
||||||
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
|
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
|
||||||
"extension": "mpd"
|
"extension": "mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Glass (WebM,VP9)",
|
"name": "Google Glass (WebM,VP9)",
|
||||||
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
|
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
|
||||||
"extension": "mpd"
|
"extension": "mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play (WebM,VP9)",
|
"name": "Google Play (WebM,VP9)",
|
||||||
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
|
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
|
||||||
"extension": "mpd"
|
"extension": "mpd"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -330,11 +330,11 @@
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Super speed",
|
"name": "Super speed",
|
||||||
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Super speed (PlayReady)",
|
"name": "Super speed (PlayReady)",
|
||||||
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
||||||
"drm_scheme": "playready"
|
"drm_scheme": "playready"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -365,10 +365,6 @@
|
||||||
{
|
{
|
||||||
"name": "Apple AAC media playlist",
|
"name": "Apple AAC media playlist",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Apple ID3 metadata",
|
|
||||||
"uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -376,7 +372,7 @@
|
||||||
"name": "Misc",
|
"name": "Misc",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Dizzy",
|
"name": "Dizzy (MP4)",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -391,10 +387,6 @@
|
||||||
"name": "Android screens (Matroska)",
|
"name": "Android screens (Matroska)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Big Buck Bunny (MP4 Video)",
|
|
||||||
"uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Screens 360P (WebM,VP9,No Audio)",
|
"name": "Screens 360P (WebM,VP9,No Audio)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
|
||||||
|
|
@ -419,21 +411,9 @@
|
||||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Google Glass (WebM Video with Vorbis Audio)",
|
|
||||||
"uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Google Glass (VP9 in MP4/ISO-BMFF)",
|
|
||||||
"uri": "http://demos.webmproject.org/exoplayer/glass.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Google Glass DASH - VP9 and Opus",
|
|
||||||
"uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (FLV Video)",
|
"name": "Big Buck Bunny (FLV Video)",
|
||||||
"uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
|
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -570,23 +550,27 @@
|
||||||
{
|
{
|
||||||
"name": "VMAP empty midroll",
|
"name": "VMAP empty midroll",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll"
|
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "VMAP full, empty, full midrolls",
|
"name": "VMAP full, empty, full midrolls",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
|
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ABR",
|
"name": "360",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Random ABR - Google Glass (MP4,H264)",
|
"name": "Congo (360 top-bottom stereo)",
|
||||||
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||||
"extension": "mpd",
|
"spherical_stereo_mode": "top_bottom"
|
||||||
"abr_algorithm": "random"
|
},
|
||||||
|
{
|
||||||
|
"name": "Iceland (360 top-bottom stereo ts)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||||
|
"spherical_stereo_mode": "top_bottom"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,13 @@
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
|
|
||||||
import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
|
|
||||||
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
|
|
@ -46,13 +40,6 @@ public class DemoApplication extends Application {
|
||||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||||
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
||||||
private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
|
|
||||||
new Deserializer[] {
|
|
||||||
DashDownloadAction.DESERIALIZER,
|
|
||||||
HlsDownloadAction.DESERIALIZER,
|
|
||||||
SsDownloadAction.DESERIALIZER,
|
|
||||||
ProgressiveDownloadAction.DESERIALIZER
|
|
||||||
};
|
|
||||||
|
|
||||||
protected String userAgent;
|
protected String userAgent;
|
||||||
|
|
||||||
|
|
@ -68,16 +55,15 @@ public class DemoApplication extends Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a {@link DataSource.Factory}. */
|
/** Returns a {@link DataSource.Factory}. */
|
||||||
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
public DataSource.Factory buildDataSourceFactory() {
|
||||||
DefaultDataSourceFactory upstreamFactory =
|
DefaultDataSourceFactory upstreamFactory =
|
||||||
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a {@link HttpDataSource.Factory}. */
|
/** Returns a {@link HttpDataSource.Factory}. */
|
||||||
public HttpDataSource.Factory buildHttpDataSourceFactory(
|
public HttpDataSource.Factory buildHttpDataSourceFactory() {
|
||||||
TransferListener<? super DataSource> listener) {
|
return new DefaultHttpDataSourceFactory(userAgent);
|
||||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether extension renderers should be used. */
|
/** Returns whether extension renderers should be used. */
|
||||||
|
|
@ -98,21 +84,18 @@ public class DemoApplication extends Application {
|
||||||
private synchronized void initDownloadManager() {
|
private synchronized void initDownloadManager() {
|
||||||
if (downloadManager == null) {
|
if (downloadManager == null) {
|
||||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||||
new DownloaderConstructorHelper(
|
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
|
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
downloaderConstructorHelper,
|
downloaderConstructorHelper,
|
||||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
|
||||||
DOWNLOAD_DESERIALIZERS);
|
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(
|
new DownloadTracker(
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
buildDataSourceFactory(/* listener= */ null),
|
buildDataSourceFactory(),
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
|
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
||||||
DOWNLOAD_DESERIALIZERS);
|
|
||||||
downloadManager.addListener(downloadTracker);
|
downloadManager.addListener(downloadTracker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.offline.TrackKey;
|
import com.google.android.exoplayer2.offline.TrackKey;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
|
@ -85,7 +85,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
Context context,
|
Context context,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
File actionFile,
|
File actionFile,
|
||||||
DownloadAction.Deserializer[] deserializers) {
|
DownloadAction.Deserializer... deserializers) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
this.actionFile = new ActionFile(actionFile);
|
this.actionFile = new ActionFile(actionFile);
|
||||||
|
|
@ -95,7 +95,8 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
||||||
actionFileWriteThread.start();
|
actionFileWriteThread.start();
|
||||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
||||||
loadTrackedActions(deserializers);
|
loadTrackedActions(
|
||||||
|
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(Listener listener) {
|
public void addListener(Listener listener) {
|
||||||
|
|
@ -111,15 +112,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <K> List<K> getOfflineStreamKeys(Uri uri) {
|
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||||
if (!trackedDownloadStates.containsKey(uri)) {
|
if (!trackedDownloadStates.containsKey(uri)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
DownloadAction action = trackedDownloadStates.get(uri);
|
return trackedDownloadStates.get(uri).getKeys();
|
||||||
if (action instanceof SegmentDownloadAction) {
|
|
||||||
return ((SegmentDownloadAction) action).keys;
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
|
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
|
||||||
|
|
@ -270,11 +267,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
|
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!trackKeys.isEmpty()) {
|
|
||||||
builder.setView(dialogView);
|
|
||||||
}
|
|
||||||
builder.create().show();
|
|
||||||
}
|
}
|
||||||
|
if (!trackKeys.isEmpty()) {
|
||||||
|
builder.setView(dialogView);
|
||||||
|
}
|
||||||
|
builder.create().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -282,6 +279,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
|
Log.e(TAG, "Failed to start download", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||||
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
||||||
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
|
|
@ -57,16 +58,11 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
|
||||||
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.RepresentationKey;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.RenditionKey;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey;
|
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
|
|
@ -77,8 +73,8 @@ import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
||||||
|
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||||
import com.google.android.exoplayer2.util.EventLogger;
|
import com.google.android.exoplayer2.util.EventLogger;
|
||||||
|
|
@ -111,8 +107,13 @@ public class PlayerActivity extends Activity
|
||||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||||
|
|
||||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||||
private static final String ABR_ALGORITHM_DEFAULT = "default";
|
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||||
private static final String ABR_ALGORITHM_RANDOM = "random";
|
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||||
|
|
||||||
|
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||||
|
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||||
|
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||||
|
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||||
|
|
||||||
// For backwards compatibility only.
|
// For backwards compatibility only.
|
||||||
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||||
|
|
@ -123,7 +124,6 @@ public class PlayerActivity extends Activity
|
||||||
private static final String KEY_POSITION = "position";
|
private static final String KEY_POSITION = "position";
|
||||||
private static final String KEY_AUTO_PLAY = "auto_play";
|
private static final String KEY_AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
|
||||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||||
static {
|
static {
|
||||||
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
||||||
|
|
@ -134,8 +134,9 @@ public class PlayerActivity extends Activity
|
||||||
private LinearLayout debugRootView;
|
private LinearLayout debugRootView;
|
||||||
private TextView debugTextView;
|
private TextView debugTextView;
|
||||||
|
|
||||||
private DataSource.Factory mediaDataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
|
private FrameworkMediaDrm mediaDrm;
|
||||||
private MediaSource mediaSource;
|
private MediaSource mediaSource;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
|
|
@ -156,8 +157,12 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||||
|
if (sphericalStereoMode != null) {
|
||||||
|
setTheme(R.style.PlayerTheme_Spherical);
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
dataSourceFactory = buildDataSourceFactory();
|
||||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +177,21 @@ public class PlayerActivity extends Activity
|
||||||
playerView.setControllerVisibilityListener(this);
|
playerView.setControllerVisibilityListener(this);
|
||||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||||
playerView.requestFocus();
|
playerView.requestFocus();
|
||||||
|
if (sphericalStereoMode != null) {
|
||||||
|
int stereoMode;
|
||||||
|
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||||
|
stereoMode = C.STEREO_MODE_MONO;
|
||||||
|
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||||
|
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||||
|
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||||
|
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||||
|
} else {
|
||||||
|
showToast(R.string.error_unrecognized_stereo_mode);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
((SphericalSurfaceView) playerView.getVideoSurfaceView()).setStereoMode(stereoMode);
|
||||||
|
}
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||||
|
|
@ -187,6 +207,7 @@ public class PlayerActivity extends Activity
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent) {
|
public void onNewIntent(Intent intent) {
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
|
releaseAdsLoader();
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +217,9 @@ public class PlayerActivity extends Activity
|
||||||
super.onStart();
|
super.onStart();
|
||||||
if (Util.SDK_INT > 23) {
|
if (Util.SDK_INT > 23) {
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onResume();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +228,9 @@ public class PlayerActivity extends Activity
|
||||||
super.onResume();
|
super.onResume();
|
||||||
if (Util.SDK_INT <= 23 || player == null) {
|
if (Util.SDK_INT <= 23 || player == null) {
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onResume();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +238,9 @@ public class PlayerActivity extends Activity
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
if (Util.SDK_INT <= 23) {
|
if (Util.SDK_INT <= 23) {
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onPause();
|
||||||
|
}
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +249,9 @@ public class PlayerActivity extends Activity
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
if (Util.SDK_INT > 23) {
|
if (Util.SDK_INT > 23) {
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onPause();
|
||||||
|
}
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +360,11 @@ public class PlayerActivity extends Activity
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
|
if (!Util.checkCleartextTrafficPermitted(uris)) {
|
||||||
|
showToast(R.string.error_cleartext_not_permitted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {
|
||||||
// The player will be reinitialized if the permission is granted.
|
// The player will be reinitialized if the permission is granted.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -368,7 +405,7 @@ public class PlayerActivity extends Activity
|
||||||
TrackSelection.Factory trackSelectionFactory;
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||||
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||||
trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||||
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -392,7 +429,8 @@ public class PlayerActivity extends Activity
|
||||||
lastSeenTrackGroupArray = null;
|
lastSeenTrackGroupArray = null;
|
||||||
|
|
||||||
player =
|
player =
|
||||||
ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
|
ExoPlayerFactory.newSimpleInstance(
|
||||||
|
/* context= */ this, renderersFactory, trackSelector, drmSessionManager);
|
||||||
player.addListener(new PlayerEventListener());
|
player.addListener(new PlayerEventListener());
|
||||||
player.setPlayWhenReady(startAutoPlay);
|
player.setPlayWhenReady(startAutoPlay);
|
||||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||||
|
|
@ -441,36 +479,29 @@ public class PlayerActivity extends Activity
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(dataSourceFactory)
|
||||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
|
||||||
buildDataSourceFactory(false))
|
|
||||||
.setManifestParser(
|
.setManifestParser(
|
||||||
new FilteringManifestParser<>(
|
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
|
||||||
new DashManifestParser(), (List<RepresentationKey>) getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(
|
return new SsMediaSource.Factory(dataSourceFactory)
|
||||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
|
||||||
buildDataSourceFactory(false))
|
|
||||||
.setManifestParser(
|
.setManifestParser(
|
||||||
new FilteringManifestParser<>(
|
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
|
||||||
new SsManifestParser(), (List<StreamKey>) getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||||
.setPlaylistParser(
|
.setPlaylistParser(
|
||||||
new FilteringManifestParser<>(
|
new FilteringManifestParser<>(new HlsPlaylistParser(), getOfflineStreamKeys(uri)))
|
||||||
new HlsPlaylistParser(), (List<RenditionKey>) getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default: {
|
default: {
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<?> getOfflineStreamKeys(Uri uri) {
|
private List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||||
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,7 +509,7 @@ public class PlayerActivity extends Activity
|
||||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||||
throws UnsupportedDrmException {
|
throws UnsupportedDrmException {
|
||||||
HttpDataSource.Factory licenseDataSourceFactory =
|
HttpDataSource.Factory licenseDataSourceFactory =
|
||||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
|
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
|
||||||
HttpMediaDrmCallback drmCallback =
|
HttpMediaDrmCallback drmCallback =
|
||||||
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||||
if (keyRequestPropertiesArray != null) {
|
if (keyRequestPropertiesArray != null) {
|
||||||
|
|
@ -487,8 +518,9 @@ public class PlayerActivity extends Activity
|
||||||
keyRequestPropertiesArray[i + 1]);
|
keyRequestPropertiesArray[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new DefaultDrmSessionManager<>(
|
releaseMediaDrm();
|
||||||
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
|
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||||
|
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
|
|
@ -502,6 +534,23 @@ public class PlayerActivity extends Activity
|
||||||
mediaSource = null;
|
mediaSource = null;
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
}
|
}
|
||||||
|
releaseMediaDrm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaDrm() {
|
||||||
|
if (mediaDrm != null) {
|
||||||
|
mediaDrm.release();
|
||||||
|
mediaDrm = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseAdsLoader() {
|
||||||
|
if (adsLoader != null) {
|
||||||
|
adsLoader.release();
|
||||||
|
adsLoader = null;
|
||||||
|
loadedAdTagUri = null;
|
||||||
|
playerView.getOverlayFrameLayout().removeAllViews();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTrackSelectorParameters() {
|
private void updateTrackSelectorParameters() {
|
||||||
|
|
@ -524,16 +573,9 @@ public class PlayerActivity extends Activity
|
||||||
startPosition = C.TIME_UNSET;
|
startPosition = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns a new DataSource factory. */
|
||||||
* Returns a new DataSource factory.
|
private DataSource.Factory buildDataSourceFactory() {
|
||||||
*
|
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||||
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
|
|
||||||
* DataSource factory.
|
|
||||||
* @return A new DataSource factory.
|
|
||||||
*/
|
|
||||||
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
|
|
||||||
return ((DemoApplication) getApplication())
|
|
||||||
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||||
|
|
@ -576,15 +618,6 @@ public class PlayerActivity extends Activity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseAdsLoader() {
|
|
||||||
if (adsLoader != null) {
|
|
||||||
adsLoader.release();
|
|
||||||
adsLoader = null;
|
|
||||||
loadedAdTagUri = null;
|
|
||||||
playerView.getOverlayFrameLayout().removeAllViews();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User controls
|
// User controls
|
||||||
|
|
||||||
private void updateButtonVisibilities() {
|
private void updateButtonVisibilities() {
|
||||||
|
|
@ -650,7 +683,7 @@ public class PlayerActivity extends Activity
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PlayerEventListener extends Player.DefaultEventListener {
|
private class PlayerEventListener implements Player.EventListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.JsonReader;
|
import android.util.JsonReader;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -55,8 +58,11 @@ public class SampleChooserActivity extends Activity
|
||||||
|
|
||||||
private static final String TAG = "SampleChooserActivity";
|
private static final String TAG = "SampleChooserActivity";
|
||||||
|
|
||||||
|
private boolean useExtensionRenderers;
|
||||||
private DownloadTracker downloadTracker;
|
private DownloadTracker downloadTracker;
|
||||||
private SampleAdapter sampleAdapter;
|
private SampleAdapter sampleAdapter;
|
||||||
|
private MenuItem preferExtensionDecodersMenuItem;
|
||||||
|
private MenuItem randomAbrMenuItem;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -90,13 +96,37 @@ public class SampleChooserActivity extends Activity
|
||||||
Arrays.sort(uris);
|
Arrays.sort(uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
|
DemoApplication application = (DemoApplication) getApplication();
|
||||||
|
useExtensionRenderers = application.useExtensionRenderers();
|
||||||
|
downloadTracker = application.getDownloadTracker();
|
||||||
SampleListLoader loaderTask = new SampleListLoader();
|
SampleListLoader loaderTask = new SampleListLoader();
|
||||||
loaderTask.execute(uris);
|
loaderTask.execute(uris);
|
||||||
|
|
||||||
// Ping the download service in case it's not running (but should be).
|
// Start the download service if it should be running but it's not currently.
|
||||||
startService(
|
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||||
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
|
// action. Starting it in the background throws an exception if the app is in the background too
|
||||||
|
// (e.g. if device screen is locked).
|
||||||
|
try {
|
||||||
|
DownloadService.start(this, DemoDownloadService.class);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
DownloadService.startForeground(this, DemoDownloadService.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.sample_chooser_menu, menu);
|
||||||
|
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
|
||||||
|
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
|
||||||
|
randomAbrMenuItem = menu.findItem(R.id.random_abr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
item.setChecked(!item.isChecked());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -129,7 +159,13 @@ public class SampleChooserActivity extends Activity
|
||||||
public boolean onChildClick(
|
public boolean onChildClick(
|
||||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||||
Sample sample = (Sample) view.getTag();
|
Sample sample = (Sample) view.getTag();
|
||||||
startActivity(sample.buildIntent(this));
|
startActivity(
|
||||||
|
sample.buildIntent(
|
||||||
|
/* context= */ this,
|
||||||
|
preferExtensionDecodersMenuItem.isChecked(),
|
||||||
|
randomAbrMenuItem.isChecked()
|
||||||
|
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||||
|
: PlayerActivity.ABR_ALGORITHM_DEFAULT));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,10 +275,9 @@ public class SampleChooserActivity extends Activity
|
||||||
String drmLicenseUrl = null;
|
String drmLicenseUrl = null;
|
||||||
String[] drmKeyRequestProperties = null;
|
String[] drmKeyRequestProperties = null;
|
||||||
boolean drmMultiSession = false;
|
boolean drmMultiSession = false;
|
||||||
boolean preferExtensionDecoders = false;
|
|
||||||
ArrayList<UriSample> playlistSamples = null;
|
ArrayList<UriSample> playlistSamples = null;
|
||||||
String adTagUri = null;
|
String adTagUri = null;
|
||||||
String abrAlgorithm = null;
|
String sphericalStereoMode = null;
|
||||||
|
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
|
|
@ -281,11 +316,6 @@ public class SampleChooserActivity extends Activity
|
||||||
case "drm_multi_session":
|
case "drm_multi_session":
|
||||||
drmMultiSession = reader.nextBoolean();
|
drmMultiSession = reader.nextBoolean();
|
||||||
break;
|
break;
|
||||||
case "prefer_extension_decoders":
|
|
||||||
Assertions.checkState(!insidePlaylist,
|
|
||||||
"Invalid attribute on nested item: prefer_extension_decoders");
|
|
||||||
preferExtensionDecoders = reader.nextBoolean();
|
|
||||||
break;
|
|
||||||
case "playlist":
|
case "playlist":
|
||||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||||
playlistSamples = new ArrayList<>();
|
playlistSamples = new ArrayList<>();
|
||||||
|
|
@ -298,10 +328,10 @@ public class SampleChooserActivity extends Activity
|
||||||
case "ad_tag_uri":
|
case "ad_tag_uri":
|
||||||
adTagUri = reader.nextString();
|
adTagUri = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "abr_algorithm":
|
case "spherical_stereo_mode":
|
||||||
Assertions.checkState(
|
Assertions.checkState(
|
||||||
!insidePlaylist, "Invalid attribute on nested item: abr_algorithm");
|
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||||
abrAlgorithm = reader.nextString();
|
sphericalStereoMode = reader.nextString();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ParserException("Unsupported attribute name: " + name);
|
throw new ParserException("Unsupported attribute name: " + name);
|
||||||
|
|
@ -315,11 +345,15 @@ public class SampleChooserActivity extends Activity
|
||||||
if (playlistSamples != null) {
|
if (playlistSamples != null) {
|
||||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
||||||
new UriSample[playlistSamples.size()]);
|
new UriSample[playlistSamples.size()]);
|
||||||
return new PlaylistSample(
|
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
|
||||||
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray);
|
|
||||||
} else {
|
} else {
|
||||||
return new UriSample(
|
return new UriSample(
|
||||||
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri);
|
sampleName,
|
||||||
|
drmInfo,
|
||||||
|
uri,
|
||||||
|
extension,
|
||||||
|
adTagUri,
|
||||||
|
sphericalStereoMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,19 +511,15 @@ public class SampleChooserActivity extends Activity
|
||||||
|
|
||||||
private abstract static class Sample {
|
private abstract static class Sample {
|
||||||
public final String name;
|
public final String name;
|
||||||
public final boolean preferExtensionDecoders;
|
|
||||||
public final String abrAlgorithm;
|
|
||||||
public final DrmInfo drmInfo;
|
public final DrmInfo drmInfo;
|
||||||
|
|
||||||
public Sample(
|
public Sample(String name, DrmInfo drmInfo) {
|
||||||
String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.preferExtensionDecoders = preferExtensionDecoders;
|
|
||||||
this.abrAlgorithm = abrAlgorithm;
|
|
||||||
this.drmInfo = drmInfo;
|
this.drmInfo = drmInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Intent buildIntent(Context context) {
|
public Intent buildIntent(
|
||||||
|
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||||
Intent intent = new Intent(context, PlayerActivity.class);
|
Intent intent = new Intent(context, PlayerActivity.class);
|
||||||
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
|
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
|
||||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||||
|
|
@ -506,27 +536,30 @@ public class SampleChooserActivity extends Activity
|
||||||
public final Uri uri;
|
public final Uri uri;
|
||||||
public final String extension;
|
public final String extension;
|
||||||
public final String adTagUri;
|
public final String adTagUri;
|
||||||
|
public final String sphericalStereoMode;
|
||||||
|
|
||||||
public UriSample(
|
public UriSample(
|
||||||
String name,
|
String name,
|
||||||
boolean preferExtensionDecoders,
|
|
||||||
String abrAlgorithm,
|
|
||||||
DrmInfo drmInfo,
|
DrmInfo drmInfo,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
String extension,
|
String extension,
|
||||||
String adTagUri) {
|
String adTagUri,
|
||||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
String sphericalStereoMode) {
|
||||||
|
super(name, drmInfo);
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
this.adTagUri = adTagUri;
|
this.adTagUri = adTagUri;
|
||||||
|
this.sphericalStereoMode = sphericalStereoMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Intent buildIntent(Context context) {
|
public Intent buildIntent(
|
||||||
return super.buildIntent(context)
|
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||||
|
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||||
.setData(uri)
|
.setData(uri)
|
||||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||||
|
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
|
||||||
.setAction(PlayerActivity.ACTION_VIEW);
|
.setAction(PlayerActivity.ACTION_VIEW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -538,23 +571,22 @@ public class SampleChooserActivity extends Activity
|
||||||
|
|
||||||
public PlaylistSample(
|
public PlaylistSample(
|
||||||
String name,
|
String name,
|
||||||
boolean preferExtensionDecoders,
|
|
||||||
String abrAlgorithm,
|
|
||||||
DrmInfo drmInfo,
|
DrmInfo drmInfo,
|
||||||
UriSample... children) {
|
UriSample... children) {
|
||||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
super(name, drmInfo);
|
||||||
this.children = children;
|
this.children = children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Intent buildIntent(Context context) {
|
public Intent buildIntent(
|
||||||
|
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||||
String[] uris = new String[children.length];
|
String[] uris = new String[children.length];
|
||||||
String[] extensions = new String[children.length];
|
String[] extensions = new String[children.length];
|
||||||
for (int i = 0; i < children.length; i++) {
|
for (int i = 0; i < children.length; i++) {
|
||||||
uris[i] = children[i].uri.toString();
|
uris[i] = children[i].uri.toString();
|
||||||
extensions[i] = children[i].extension;
|
extensions[i] = children[i].extension;
|
||||||
}
|
}
|
||||||
return super.buildIntent(context)
|
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||||
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
|
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
|
||||||
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
|
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
|
||||||
.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
||||||
|
|
|
||||||
25
demos/main/src/main/res/menu/sample_chooser_menu.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/prefer_extension_decoders"
|
||||||
|
android:title="@string/prefer_extension_decoders"
|
||||||
|
android:showAsAction="never"
|
||||||
|
android:checkable="true"/>
|
||||||
|
<item android:id="@+id/random_abr"
|
||||||
|
android:title="@string/random_abr"
|
||||||
|
android:showAsAction="never"
|
||||||
|
android:checkable="true"/>
|
||||||
|
</menu>
|
||||||
|
|
@ -19,10 +19,14 @@
|
||||||
|
|
||||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||||
|
|
||||||
|
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||||
|
|
||||||
<string name="error_generic">Playback failed</string>
|
<string name="error_generic">Playback failed</string>
|
||||||
|
|
||||||
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||||
|
|
||||||
|
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||||
|
|
||||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||||
|
|
||||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||||
|
|
@ -57,4 +61,8 @@
|
||||||
|
|
||||||
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
||||||
|
|
||||||
|
<string name="prefer_extension_decoders">Prefer extension decoders</string>
|
||||||
|
|
||||||
|
<string name="random_abr">Enable random ABR</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,8 @@
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PlayerTheme.Spherical">
|
||||||
|
<item name="surface_type">spherical_view</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
@ -26,17 +31,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// These dependencies are necessary to force the supportLibraryVersion of
|
api 'com.google.android.gms:play-services-cast-framework:16.0.1'
|
||||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
|
||||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
|
||||||
// used, for example:
|
|
||||||
// com.google.android.gms:play-services-cast-framework:12.0.0
|
|
||||||
// |-- com.google.android.gms:play-services-basement:12.0.0
|
|
||||||
// |-- com.android.support:support-v4:26.1.0
|
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
|
||||||
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
|
@ -44,8 +39,19 @@ dependencies {
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
|
// These dependencies are necessary to force the supportLibraryVersion of
|
||||||
|
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||||
|
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
||||||
|
// used, for example via:
|
||||||
|
// com.google.android.gms:play-services-cast-framework:15.0.1
|
||||||
|
// |-- com.android.support:mediarouter-v7:26.1.0
|
||||||
|
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||||
|
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||||
|
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
javadocTitle = 'Cast extension'
|
javadocTitle = 'Cast extension'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,11 @@ public final class CastPlayer implements Player {
|
||||||
|
|
||||||
// Player implementation.
|
// Player implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioComponent getAudioComponent() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoComponent getVideoComponent() {
|
public VideoComponent getVideoComponent() {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -526,6 +531,15 @@ public final class CastPlayer implements Player {
|
||||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalBufferedDuration() {
|
||||||
|
long bufferedPosition = getBufferedPosition();
|
||||||
|
long currentPosition = getCurrentPosition();
|
||||||
|
return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
|
||||||
|
? 0
|
||||||
|
: bufferedPosition - currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCurrentWindowDynamic() {
|
public boolean isCurrentWindowDynamic() {
|
||||||
return !currentTimeline.isEmpty()
|
return !currentTimeline.isEmpty()
|
||||||
|
|
@ -563,6 +577,11 @@ public final class CastPlayer implements Player {
|
||||||
return getCurrentPosition();
|
return getCurrentPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getContentBufferedPosition() {
|
||||||
|
return getBufferedPosition();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
public void updateInternalState() {
|
public void updateInternalState() {
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ import java.util.Map;
|
||||||
/* package */ final class CastTimeline extends Timeline {
|
/* package */ final class CastTimeline extends Timeline {
|
||||||
|
|
||||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||||
new CastTimeline(
|
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
|
||||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
|
||||||
|
|
||||||
private final SparseIntArray idsToIndex;
|
private final SparseIntArray idsToIndex;
|
||||||
private final int[] ids;
|
private final int[] ids;
|
||||||
|
|
@ -108,6 +107,11 @@ import java.util.Map;
|
||||||
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getUidOfPeriod(int periodIndex) {
|
||||||
|
return ids[periodIndex];
|
||||||
|
}
|
||||||
|
|
||||||
// equals and hashCode implementations.
|
// equals and hashCode implementations.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack;
|
||||||
* @return The equivalent {@link Format}.
|
* @return The equivalent {@link Format}.
|
||||||
*/
|
*/
|
||||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||||
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
|
return Format.createContainerFormat(
|
||||||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
mediaTrack.getContentId(),
|
||||||
|
/* label= */ null,
|
||||||
|
mediaTrack.getContentType(),
|
||||||
|
/* sampleMimeType= */ null,
|
||||||
|
/* codecs= */ null,
|
||||||
|
/* bitrate= */ Format.NO_VALUE,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
mediaTrack.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CastUtils() {}
|
private CastUtils() {}
|
||||||
|
|
|
||||||
|
|
@ -5,37 +5,22 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
|
||||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||||
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
||||||
|
|
||||||
## Build instructions ##
|
## Getting the extension ##
|
||||||
|
|
||||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
The easiest way to use the extension is to add it as a gradle dependency:
|
||||||
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
|
||||||
[top level README][]. In addition, it's necessary to get the Cronet libraries
|
|
||||||
and enable the extension:
|
|
||||||
|
|
||||||
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
|
```gradle
|
||||||
directory
|
implementation 'com.google.android.exoplayer:extension-cronet:2.X.X'
|
||||||
1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
|
```
|
||||||
`cronet_impl_native_java.jar` and the `libs` directory
|
|
||||||
1. Copy the three jar files into the `libs` directory of this extension
|
where `2.X.X` is the version, which must match the version of the ExoPlayer
|
||||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
library being used.
|
||||||
directory of this extension
|
|
||||||
1. In your `settings.gradle` file, add
|
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
applies `core_settings.gradle`.
|
[top level README][].
|
||||||
1. In all `build.gradle` files where this extension is linked as a dependency,
|
|
||||||
add
|
|
||||||
```
|
|
||||||
android {
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
to enable Java 8 features required by the Cronet library.
|
|
||||||
|
|
||||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
|
||||||
|
|
||||||
## Using the extension ##
|
## Using the extension ##
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,10 @@ android {
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion 16
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main {
|
|
||||||
jniLibs.srcDirs = ['jniLibs']
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -34,9 +30,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api files('libs/cronet_api.jar')
|
api 'org.chromium.net:cronet-embedded:66.3359.158'
|
||||||
implementation files('libs/cronet_impl_common_java.jar')
|
|
||||||
implementation files('libs/cronet_impl_native_java.jar')
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
testImplementation project(modulePrefix + 'library')
|
testImplementation project(modulePrefix + 'library')
|
||||||
|
|
@ -47,3 +41,9 @@ ext {
|
||||||
javadocTitle = 'Cronet extension'
|
javadocTitle = 'Cronet extension'
|
||||||
}
|
}
|
||||||
apply from: '../../javadoc_library.gradle'
|
apply from: '../../javadoc_library.gradle'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
releaseArtifact = 'extension-cronet'
|
||||||
|
releaseDescription = 'Cronet extension for ExoPlayer.'
|
||||||
|
}
|
||||||
|
apply from: '../../publish.gradle'
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Copy folders containing architecture specific .so files here.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Copy cronet.jar and cronet_api.jar here.
|
|
||||||
|
|
@ -20,10 +20,10 @@ import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
|
|
@ -32,6 +32,7 @@ import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
@ -47,9 +48,10 @@ import org.chromium.net.UrlResponseInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
|
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
|
||||||
|
*
|
||||||
* <p>This class's methods are organized in the sequence of expected calls.
|
* <p>This class's methods are organized in the sequence of expected calls.
|
||||||
*/
|
*/
|
||||||
public class CronetDataSource extends UrlRequest.Callback implements HttpDataSource {
|
public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
|
* Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
|
||||||
|
|
@ -95,6 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
*/
|
*/
|
||||||
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
|
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
|
||||||
|
|
||||||
|
/* package */ final UrlRequest.Callback urlRequestCallback;
|
||||||
|
|
||||||
private static final String TAG = "CronetDataSource";
|
private static final String TAG = "CronetDataSource";
|
||||||
private static final String CONTENT_TYPE = "Content-Type";
|
private static final String CONTENT_TYPE = "Content-Type";
|
||||||
private static final String SET_COOKIE = "Set-Cookie";
|
private static final String SET_COOKIE = "Set-Cookie";
|
||||||
|
|
@ -108,7 +112,6 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
private final CronetEngine cronetEngine;
|
private final CronetEngine cronetEngine;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Predicate<String> contentTypePredicate;
|
private final Predicate<String> contentTypePredicate;
|
||||||
private final TransferListener<? super CronetDataSource> listener;
|
|
||||||
private final int connectTimeoutMs;
|
private final int connectTimeoutMs;
|
||||||
private final int readTimeoutMs;
|
private final int readTimeoutMs;
|
||||||
private final boolean resetTimeoutOnRedirects;
|
private final boolean resetTimeoutOnRedirects;
|
||||||
|
|
@ -143,57 +146,73 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param cronetEngine A CronetEngine.
|
* @param cronetEngine A CronetEngine.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* to make sure response handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link #open(DataSpec)}.
|
* #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
public CronetDataSource(
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
|
CronetEngine cronetEngine, Executor executor, Predicate<String> contentTypePredicate) {
|
||||||
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
this(
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null, false);
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param cronetEngine A CronetEngine.
|
* @param cronetEngine A CronetEngine.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* to make sure response handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link #open(DataSpec)}.
|
* #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
* @param defaultRequestProperties The default request properties to be used.
|
* @param defaultRequestProperties The default request properties to be used.
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
public CronetDataSource(
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
CronetEngine cronetEngine,
|
||||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
RequestProperties defaultRequestProperties) {
|
RequestProperties defaultRequestProperties) {
|
||||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
this(
|
||||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false);
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
defaultRequestProperties,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param cronetEngine A CronetEngine.
|
* @param cronetEngine A CronetEngine.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* to make sure response handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link #open(DataSpec)}.
|
* #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
|
@ -201,23 +220,42 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||||
* the redirect url in the "Cookie" header.
|
* the redirect url in the "Cookie" header.
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
public CronetDataSource(
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
CronetEngine cronetEngine,
|
||||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
Executor executor,
|
||||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
Predicate<String> contentTypePredicate,
|
||||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties,
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
RequestProperties defaultRequestProperties,
|
||||||
|
boolean handleSetCookieRequests) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
defaultRequestProperties,
|
||||||
handleSetCookieRequests);
|
handleSetCookieRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
/* package */ CronetDataSource(
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
CronetEngine cronetEngine,
|
||||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
|
Executor executor,
|
||||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
Clock clock,
|
||||||
|
RequestProperties defaultRequestProperties,
|
||||||
|
boolean handleSetCookieRequests) {
|
||||||
|
super(/* isNetwork= */ true);
|
||||||
|
this.urlRequestCallback = new UrlRequestCallback();
|
||||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||||
this.executor = Assertions.checkNotNull(executor);
|
this.executor = Assertions.checkNotNull(executor);
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
|
||||||
this.connectTimeoutMs = connectTimeoutMs;
|
this.connectTimeoutMs = connectTimeoutMs;
|
||||||
this.readTimeoutMs = readTimeoutMs;
|
this.readTimeoutMs = readTimeoutMs;
|
||||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||||
|
|
@ -247,7 +285,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, List<String>> getResponseHeaders() {
|
public Map<String, List<String>> getResponseHeaders() {
|
||||||
return responseInfo == null ? null : responseInfo.getAllHeaders();
|
return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -270,6 +308,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
}
|
}
|
||||||
currentUrlRequest.start();
|
currentUrlRequest.start();
|
||||||
|
|
||||||
|
transferInitializing(dataSpec);
|
||||||
try {
|
try {
|
||||||
boolean connectionOpened = blockUntilConnectTimeout();
|
boolean connectionOpened = blockUntilConnectTimeout();
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
|
|
@ -323,9 +362,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
}
|
}
|
||||||
|
|
||||||
opened = true;
|
opened = true;
|
||||||
if (listener != null) {
|
transferStarted(dataSpec);
|
||||||
listener.onTransferStart(this, dataSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytesRemaining;
|
return bytesRemaining;
|
||||||
}
|
}
|
||||||
|
|
@ -391,9 +428,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
bytesRemaining -= bytesRead;
|
bytesRemaining -= bytesRead;
|
||||||
}
|
}
|
||||||
if (listener != null) {
|
bytesTransferred(bytesRead);
|
||||||
listener.onBytesTransferred(this, bytesRead);
|
|
||||||
}
|
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,107 +447,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
finished = false;
|
finished = false;
|
||||||
if (opened) {
|
if (opened) {
|
||||||
opened = false;
|
opened = false;
|
||||||
if (listener != null) {
|
transferEnded();
|
||||||
listener.onTransferEnd(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlRequest.Callback implementation
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info,
|
|
||||||
String newLocationUrl) {
|
|
||||||
if (request != currentUrlRequest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentDataSpec.postBody != null) {
|
|
||||||
int responseCode = info.getHttpStatusCode();
|
|
||||||
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
|
|
||||||
// For other redirect response codes the POST request is converted to a GET request and the
|
|
||||||
// redirect is followed.
|
|
||||||
if (responseCode == 307 || responseCode == 308) {
|
|
||||||
exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(),
|
|
||||||
currentDataSpec);
|
|
||||||
operation.open();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resetTimeoutOnRedirects) {
|
|
||||||
resetConnectTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, List<String>> headers = info.getAllHeaders();
|
|
||||||
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
|
||||||
request.followRedirect();
|
|
||||||
} else {
|
|
||||||
currentUrlRequest.cancel();
|
|
||||||
DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl),
|
|
||||||
currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition,
|
|
||||||
currentDataSpec.position, currentDataSpec.length, currentDataSpec.key,
|
|
||||||
currentDataSpec.flags);
|
|
||||||
UrlRequest.Builder requestBuilder;
|
|
||||||
try {
|
|
||||||
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
|
||||||
} catch (IOException e) {
|
|
||||||
exception = e;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
|
||||||
attachCookies(requestBuilder, cookieHeadersValue);
|
|
||||||
currentUrlRequest = requestBuilder.build();
|
|
||||||
currentUrlRequest.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
|
|
||||||
if (request != currentUrlRequest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
responseInfo = info;
|
|
||||||
operation.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info,
|
|
||||||
ByteBuffer buffer) {
|
|
||||||
if (request != currentUrlRequest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
operation.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
|
|
||||||
if (request != currentUrlRequest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
finished = true;
|
|
||||||
operation.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
|
|
||||||
CronetException error) {
|
|
||||||
if (request != currentUrlRequest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (error instanceof NetworkException
|
|
||||||
&& ((NetworkException) error).getErrorCode()
|
|
||||||
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
|
|
||||||
exception = new UnknownHostException();
|
|
||||||
} else {
|
|
||||||
exception = error;
|
|
||||||
}
|
|
||||||
operation.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
||||||
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
|
UrlRequest.Builder requestBuilder =
|
||||||
dataSpec.uri.toString(), this, executor).allowDirectExecutor();
|
cronetEngine
|
||||||
|
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
|
||||||
|
.allowDirectExecutor();
|
||||||
// Set the headers.
|
// Set the headers.
|
||||||
boolean isContentTypeHeaderSet = false;
|
boolean isContentTypeHeaderSet = false;
|
||||||
if (defaultRequestProperties != null) {
|
if (defaultRequestProperties != null) {
|
||||||
|
|
@ -528,8 +473,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||||
}
|
}
|
||||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
|
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||||
throw new IOException("POST request with non-empty body must set Content-Type");
|
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||||
}
|
}
|
||||||
// Set the Range header.
|
// Set the Range header.
|
||||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||||
|
|
@ -549,12 +494,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
// requestBuilder.addHeader("Accept-Encoding", "identity");
|
// requestBuilder.addHeader("Accept-Encoding", "identity");
|
||||||
// }
|
// }
|
||||||
// Set the method and (if non-empty) the body.
|
// Set the method and (if non-empty) the body.
|
||||||
if (dataSpec.postBody != null) {
|
requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
|
||||||
requestBuilder.setHttpMethod("POST");
|
if (dataSpec.httpBody != null) {
|
||||||
if (dataSpec.postBody.length != 0) {
|
requestBuilder.setUploadDataProvider(
|
||||||
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
|
new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
|
||||||
executor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return requestBuilder;
|
return requestBuilder;
|
||||||
}
|
}
|
||||||
|
|
@ -655,4 +598,91 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||||
return list == null || list.isEmpty();
|
return list == null || list.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class UrlRequestCallback extends UrlRequest.Callback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onRedirectReceived(
|
||||||
|
UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
|
||||||
|
if (request != currentUrlRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentDataSpec.postBody != null) {
|
||||||
|
int responseCode = info.getHttpStatusCode();
|
||||||
|
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
|
||||||
|
// For other redirect response codes the POST request is converted to a GET request and the
|
||||||
|
// redirect is followed.
|
||||||
|
if (responseCode == 307 || responseCode == 308) {
|
||||||
|
exception =
|
||||||
|
new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec);
|
||||||
|
operation.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resetTimeoutOnRedirects) {
|
||||||
|
resetConnectTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> headers = info.getAllHeaders();
|
||||||
|
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
||||||
|
request.followRedirect();
|
||||||
|
} else {
|
||||||
|
currentUrlRequest.cancel();
|
||||||
|
DataSpec redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
|
||||||
|
UrlRequest.Builder requestBuilder;
|
||||||
|
try {
|
||||||
|
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
||||||
|
} catch (IOException e) {
|
||||||
|
exception = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
||||||
|
attachCookies(requestBuilder, cookieHeadersValue);
|
||||||
|
currentUrlRequest = requestBuilder.build();
|
||||||
|
currentUrlRequest.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
|
||||||
|
if (request != currentUrlRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responseInfo = info;
|
||||||
|
operation.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onReadCompleted(
|
||||||
|
UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) {
|
||||||
|
if (request != currentUrlRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
operation.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
|
||||||
|
if (request != currentUrlRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finished = true;
|
||||||
|
operation.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onFailed(
|
||||||
|
UrlRequest request, UrlResponseInfo info, CronetException error) {
|
||||||
|
if (request != currentUrlRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error instanceof NetworkException
|
||||||
|
&& ((NetworkException) error).getErrorCode()
|
||||||
|
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
|
||||||
|
exception = new UnknownHostException();
|
||||||
|
} else {
|
||||||
|
exception = error;
|
||||||
|
}
|
||||||
|
operation.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
|
|
@ -46,7 +46,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
private final CronetEngineWrapper cronetEngineWrapper;
|
private final CronetEngineWrapper cronetEngineWrapper;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Predicate<String> contentTypePredicate;
|
private final Predicate<String> contentTypePredicate;
|
||||||
private final TransferListener<? super DataSource> transferListener;
|
private final @Nullable TransferListener transferListener;
|
||||||
private final int connectTimeoutMs;
|
private final int connectTimeoutMs;
|
||||||
private final int readTimeoutMs;
|
private final int readTimeoutMs;
|
||||||
private final boolean resetTimeoutOnRedirects;
|
private final boolean resetTimeoutOnRedirects;
|
||||||
|
|
@ -54,26 +54,176 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a CronetDataSourceFactory.
|
* Constructs a CronetDataSourceFactory.
|
||||||
* <p>
|
*
|
||||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
*
|
*
|
||||||
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
* cross-protocol redirects.
|
* cross-protocol redirects.
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link CronetDataSource#open}.
|
* CronetDataSource#open}.
|
||||||
* @param transferListener An optional listener.
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
* suitable CronetEngine can be build.
|
||||||
* no suitable CronetEngine can be build.
|
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
public CronetDataSourceFactory(
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
TransferListener<? super DataSource> transferListener,
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
|
this(
|
||||||
|
cronetEngineWrapper,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
/* transferListener= */ null,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false,
|
||||||
|
fallbackFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
*
|
||||||
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||||
|
* DefaultHttpDataSourceFactory} will be used instead.
|
||||||
|
*
|
||||||
|
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||||
|
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
|
* cross-protocol redirects.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* CronetDataSource#open}.
|
||||||
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(
|
||||||
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
String userAgent) {
|
||||||
|
this(
|
||||||
|
cronetEngineWrapper,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
/* transferListener= */ null,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false,
|
||||||
|
new DefaultHttpDataSourceFactory(
|
||||||
|
userAgent,
|
||||||
|
/* listener= */ null,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
*
|
||||||
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||||
|
* DefaultHttpDataSourceFactory} will be used instead.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* CronetDataSource#open}.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(
|
||||||
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
String userAgent) {
|
||||||
|
this(
|
||||||
|
cronetEngineWrapper,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
/* transferListener= */ null,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
new DefaultHttpDataSourceFactory(
|
||||||
|
userAgent,
|
||||||
|
/* listener= */ null,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
*
|
||||||
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* CronetDataSource#open}.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
|
* suitable CronetEngine can be build.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(
|
||||||
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
|
this(
|
||||||
|
cronetEngineWrapper,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
/* transferListener= */ null,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
fallbackFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
*
|
||||||
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
|
*
|
||||||
|
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||||
|
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
|
* cross-protocol redirects.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* CronetDataSource#open}.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
|
* suitable CronetEngine can be build.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(
|
||||||
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
@Nullable TransferListener transferListener,
|
||||||
HttpDataSource.Factory fallbackFactory) {
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
||||||
|
|
@ -81,25 +231,28 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a CronetDataSourceFactory.
|
* Constructs a CronetDataSourceFactory.
|
||||||
* <p>
|
|
||||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
|
||||||
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
|
||||||
*
|
*
|
||||||
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
* DefaultHttpDataSourceFactory} will be used instead.
|
||||||
|
*
|
||||||
|
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||||
|
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
* cross-protocol redirects.
|
* cross-protocol redirects.
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link CronetDataSource#open}.
|
* CronetDataSource#open}.
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
public CronetDataSourceFactory(
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
TransferListener<? super DataSource> transferListener, String userAgent) {
|
Executor executor,
|
||||||
|
Predicate<String> contentTypePredicate,
|
||||||
|
@Nullable TransferListener transferListener,
|
||||||
|
String userAgent) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
||||||
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
||||||
|
|
@ -108,25 +261,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a CronetDataSourceFactory.
|
* Constructs a CronetDataSourceFactory.
|
||||||
* <p>
|
*
|
||||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||||
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
* DefaultHttpDataSourceFactory} will be used instead.
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link CronetDataSource#open}.
|
* CronetDataSource#open}.
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
public CronetDataSourceFactory(
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
Executor executor,
|
||||||
int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
|
Predicate<String> contentTypePredicate,
|
||||||
|
@Nullable TransferListener transferListener,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
String userAgent) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
||||||
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
||||||
|
|
@ -135,26 +293,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a CronetDataSourceFactory.
|
* Constructs a CronetDataSourceFactory.
|
||||||
* <p>
|
*
|
||||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link CronetDataSource#open}.
|
* CronetDataSource#open}.
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
* no suitable CronetEngine can be build.
|
* suitable CronetEngine can be build.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
public CronetDataSourceFactory(
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
Executor executor,
|
||||||
int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
Predicate<String> contentTypePredicate,
|
||||||
|
@Nullable TransferListener transferListener,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
HttpDataSource.Factory fallbackFactory) {
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
this.cronetEngineWrapper = cronetEngineWrapper;
|
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
|
|
@ -173,8 +335,19 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
if (cronetEngine == null) {
|
if (cronetEngine == null) {
|
||||||
return fallbackFactory.createDataSource();
|
return fallbackFactory.createDataSource();
|
||||||
}
|
}
|
||||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
CronetDataSource dataSource =
|
||||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
|
new CronetDataSource(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
defaultRequestProperties);
|
||||||
|
if (transferListener != null) {
|
||||||
|
dataSource.addTransferListener(transferListener);
|
||||||
|
}
|
||||||
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
@ -81,13 +80,14 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
private DataSpec testDataSpec;
|
private DataSpec testDataSpec;
|
||||||
private DataSpec testPostDataSpec;
|
private DataSpec testPostDataSpec;
|
||||||
|
private DataSpec testHeadDataSpec;
|
||||||
private Map<String, String> testResponseHeader;
|
private Map<String, String> testResponseHeader;
|
||||||
private UrlResponseInfo testUrlResponseInfo;
|
private UrlResponseInfo testUrlResponseInfo;
|
||||||
|
|
||||||
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
||||||
@Mock private UrlRequest mockUrlRequest;
|
@Mock private UrlRequest mockUrlRequest;
|
||||||
@Mock private Predicate<String> mockContentTypePredicate;
|
@Mock private Predicate<String> mockContentTypePredicate;
|
||||||
@Mock private TransferListener<CronetDataSource> mockTransferListener;
|
@Mock private TransferListener mockTransferListener;
|
||||||
@Mock private Executor mockExecutor;
|
@Mock private Executor mockExecutor;
|
||||||
@Mock private NetworkException mockNetworkException;
|
@Mock private NetworkException mockNetworkException;
|
||||||
@Mock private CronetEngine mockCronetEngine;
|
@Mock private CronetEngine mockCronetEngine;
|
||||||
|
|
@ -99,18 +99,17 @@ public final class CronetDataSourceTest {
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
spy(
|
new CronetDataSource(
|
||||||
new CronetDataSource(
|
mockCronetEngine,
|
||||||
mockCronetEngine,
|
mockExecutor,
|
||||||
mockExecutor,
|
mockContentTypePredicate,
|
||||||
mockContentTypePredicate,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
mockTransferListener,
|
TEST_READ_TIMEOUT_MS,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
true, // resetTimeoutOnRedirects
|
||||||
TEST_READ_TIMEOUT_MS,
|
Clock.DEFAULT,
|
||||||
true, // resetTimeoutOnRedirects
|
null,
|
||||||
Clock.DEFAULT,
|
false);
|
||||||
null,
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
false));
|
|
||||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||||
when(mockCronetEngine.newUrlRequestBuilder(
|
when(mockCronetEngine.newUrlRequestBuilder(
|
||||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||||
|
|
@ -122,6 +121,9 @@ public final class CronetDataSourceTest {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
|
||||||
testPostDataSpec =
|
testPostDataSpec =
|
||||||
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
|
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||||
|
testHeadDataSpec =
|
||||||
|
new DataSpec(
|
||||||
|
Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||||
testResponseHeader = new HashMap<>();
|
testResponseHeader = new HashMap<>();
|
||||||
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
||||||
// This value can be anything since the DataSpec is unset.
|
// This value can be anything since the DataSpec is unset.
|
||||||
|
|
@ -172,9 +174,10 @@ public final class CronetDataSourceTest {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
// Invoke the callback for the previous request.
|
// Invoke the callback for the previous request.
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||||
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
|
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
|
||||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||||
|
mockUrlRequest2, testUrlResponseInfo);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -213,7 +216,8 @@ public final class CronetDataSourceTest {
|
||||||
public void testRequestOpen() throws HttpDataSourceException {
|
public void testRequestOpen() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener)
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -225,7 +229,8 @@ public final class CronetDataSourceTest {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
|
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
|
||||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener)
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -239,7 +244,8 @@ public final class CronetDataSourceTest {
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
|
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +262,8 @@ public final class CronetDataSourceTest {
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
|
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +279,8 @@ public final class CronetDataSourceTest {
|
||||||
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
|
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,7 +306,8 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
|
verify(mockTransferListener)
|
||||||
|
.onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -327,6 +336,15 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHeadRequestOpen() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
dataSourceUnderTest.open(testHeadDataSpec);
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);
|
||||||
|
dataSourceUnderTest.close();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
public void testRequestReadTwice() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
@ -346,7 +364,8 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
// Should have only called read on cronet once.
|
// Should have only called read on cronet once.
|
||||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||||
verify(mockTransferListener, times(2)).onBytesTransferred(dataSourceUnderTest, 8);
|
verify(mockTransferListener, times(2))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -386,7 +405,8 @@ public final class CronetDataSourceTest {
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||||
assertThat(bytesRead).isEqualTo(8);
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -402,7 +422,8 @@ public final class CronetDataSourceTest {
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
assertThat(bytesRead).isEqualTo(16);
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -418,7 +439,8 @@ public final class CronetDataSourceTest {
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
assertThat(bytesRead).isEqualTo(16);
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -433,7 +455,8 @@ public final class CronetDataSourceTest {
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||||
assertThat(bytesRead).isEqualTo(8);
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -447,7 +470,8 @@ public final class CronetDataSourceTest {
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
|
||||||
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
|
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
|
||||||
assertThat(bytesRead).isEqualTo(16);
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -464,7 +488,8 @@ public final class CronetDataSourceTest {
|
||||||
assertThat(bytesRead).isEqualTo(8);
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
|
||||||
dataSourceUnderTest.close();
|
dataSourceUnderTest.close();
|
||||||
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
|
verify(mockTransferListener)
|
||||||
|
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||||
|
|
@ -505,9 +530,12 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
// Should have only called read on cronet once.
|
// Should have only called read on cronet once.
|
||||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 8);
|
verify(mockTransferListener, times(1))
|
||||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 6);
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 2);
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
|
||||||
|
|
||||||
// Now we already returned the 16 bytes initially asked.
|
// Now we already returned the 16 bytes initially asked.
|
||||||
// Try to read again even though all requested 16 bytes are already returned.
|
// Try to read again even though all requested 16 bytes are already returned.
|
||||||
|
|
@ -518,7 +546,8 @@ public final class CronetDataSourceTest {
|
||||||
assertThat(returnedBuffer).isEqualTo(new byte[16]);
|
assertThat(returnedBuffer).isEqualTo(new byte[16]);
|
||||||
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
.onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT);
|
.onBytesTransferred(
|
||||||
|
dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
|
||||||
// There should still be only one call to read on cronet.
|
// There should still be only one call to read on cronet.
|
||||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
|
|
@ -559,7 +588,8 @@ public final class CronetDataSourceTest {
|
||||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
|
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -597,11 +627,12 @@ public final class CronetDataSourceTest {
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
|
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectResponseBeforeTimeout() throws InterruptedException {
|
public void testConnectResponseBeforeTimeout() throws Exception {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch openLatch = new CountDownLatch(1);
|
final CountDownLatch openLatch = new CountDownLatch(1);
|
||||||
|
|
@ -625,12 +656,12 @@ public final class CronetDataSourceTest {
|
||||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||||
assertNotCountedDown(openLatch);
|
assertNotCountedDown(openLatch);
|
||||||
// The response arrives just in time.
|
// The response arrives just in time.
|
||||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||||
openLatch.await();
|
openLatch.await();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
|
public void testRedirectIncreasesConnectionTimeout() throws Exception {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||||
|
|
@ -659,7 +690,7 @@ public final class CronetDataSourceTest {
|
||||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// A redirect arrives just in time.
|
// A redirect arrives just in time.
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
||||||
|
|
||||||
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
||||||
|
|
@ -667,7 +698,7 @@ public final class CronetDataSourceTest {
|
||||||
// We should still be trying to open as we approach the new timeout.
|
// We should still be trying to open as we approach the new timeout.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// A redirect arrives just in time.
|
// A redirect arrives just in time.
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
||||||
|
|
||||||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
||||||
|
|
@ -678,7 +709,8 @@ public final class CronetDataSourceTest {
|
||||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
|
|
||||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
verify(mockTransferListener, never())
|
||||||
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
assertThat(openExceptions.get()).isEqualTo(1);
|
assertThat(openExceptions.get()).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -700,18 +732,17 @@ public final class CronetDataSourceTest {
|
||||||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
spy(
|
new CronetDataSource(
|
||||||
new CronetDataSource(
|
mockCronetEngine,
|
||||||
mockCronetEngine,
|
mockExecutor,
|
||||||
mockExecutor,
|
mockContentTypePredicate,
|
||||||
mockContentTypePredicate,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
mockTransferListener,
|
TEST_READ_TIMEOUT_MS,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
true, // resetTimeoutOnRedirects
|
||||||
TEST_READ_TIMEOUT_MS,
|
Clock.DEFAULT,
|
||||||
true, // resetTimeoutOnRedirects
|
null,
|
||||||
Clock.DEFAULT,
|
true);
|
||||||
null,
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
true));
|
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
|
|
@ -732,18 +763,17 @@ public final class CronetDataSourceTest {
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
spy(
|
new CronetDataSource(
|
||||||
new CronetDataSource(
|
mockCronetEngine,
|
||||||
mockCronetEngine,
|
mockExecutor,
|
||||||
mockExecutor,
|
mockContentTypePredicate,
|
||||||
mockContentTypePredicate,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
mockTransferListener,
|
TEST_READ_TIMEOUT_MS,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
true, // resetTimeoutOnRedirects
|
||||||
TEST_READ_TIMEOUT_MS,
|
Clock.DEFAULT,
|
||||||
true, // resetTimeoutOnRedirects
|
null,
|
||||||
Clock.DEFAULT,
|
true);
|
||||||
null,
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
true));
|
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
|
|
@ -772,18 +802,17 @@ public final class CronetDataSourceTest {
|
||||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
spy(
|
new CronetDataSource(
|
||||||
new CronetDataSource(
|
mockCronetEngine,
|
||||||
mockCronetEngine,
|
mockExecutor,
|
||||||
mockExecutor,
|
mockContentTypePredicate,
|
||||||
mockContentTypePredicate,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
mockTransferListener,
|
TEST_READ_TIMEOUT_MS,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
true, // resetTimeoutOnRedirects
|
||||||
TEST_READ_TIMEOUT_MS,
|
Clock.DEFAULT,
|
||||||
true, // resetTimeoutOnRedirects
|
null,
|
||||||
Clock.DEFAULT,
|
true);
|
||||||
null,
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
true));
|
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
mockFollowRedirectSuccess();
|
mockFollowRedirectSuccess();
|
||||||
|
|
||||||
|
|
@ -800,7 +829,7 @@ public final class CronetDataSourceTest {
|
||||||
// the subsequent open() call succeeds.
|
// the subsequent open() call succeeds.
|
||||||
doThrow(new NullPointerException())
|
doThrow(new NullPointerException())
|
||||||
.when(mockTransferListener)
|
.when(mockTransferListener)
|
||||||
.onTransferEnd(dataSourceUnderTest);
|
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
try {
|
try {
|
||||||
dataSourceUnderTest.close();
|
dataSourceUnderTest.close();
|
||||||
|
|
@ -889,7 +918,8 @@ public final class CronetDataSourceTest {
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||||
|
mockUrlRequest, testUrlResponseInfo);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -902,7 +932,7 @@ public final class CronetDataSourceTest {
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfo(307), // statusCode
|
createUrlResponseInfo(307), // statusCode
|
||||||
"http://redirect.location.com");
|
"http://redirect.location.com");
|
||||||
|
|
@ -920,12 +950,13 @@ public final class CronetDataSourceTest {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
if (!redirectCalled) {
|
if (!redirectCalled) {
|
||||||
redirectCalled = true;
|
redirectCalled = true;
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||||
"http://example.com/video/redirect");
|
"http://example.com/video/redirect");
|
||||||
} else {
|
} else {
|
||||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||||
|
mockUrlRequest, testUrlResponseInfo);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -939,7 +970,8 @@ public final class CronetDataSourceTest {
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||||
|
mockUrlRequest, testUrlResponseInfo);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -952,7 +984,7 @@ public final class CronetDataSourceTest {
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfo(500), // statusCode
|
createUrlResponseInfo(500), // statusCode
|
||||||
mockNetworkException);
|
mockNetworkException);
|
||||||
|
|
@ -970,14 +1002,15 @@ public final class CronetDataSourceTest {
|
||||||
@Override
|
@Override
|
||||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||||
if (positionAndRemaining[1] == 0) {
|
if (positionAndRemaining[1] == 0) {
|
||||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onSucceeded(
|
||||||
|
mockUrlRequest, testUrlResponseInfo);
|
||||||
} else {
|
} else {
|
||||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||||
positionAndRemaining[0] += readLength;
|
positionAndRemaining[0] += readLength;
|
||||||
positionAndRemaining[1] -= readLength;
|
positionAndRemaining[1] -= readLength;
|
||||||
dataSourceUnderTest.onReadCompleted(
|
dataSourceUnderTest.urlRequestCallback.onReadCompleted(
|
||||||
mockUrlRequest, testUrlResponseInfo, inputBuffer);
|
mockUrlRequest, testUrlResponseInfo, inputBuffer);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -992,7 +1025,7 @@ public final class CronetDataSourceTest {
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfo(500), // statusCode
|
createUrlResponseInfo(500), // statusCode
|
||||||
mockNetworkException);
|
mockNetworkException);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ COMMON_OPTIONS="\
|
||||||
--enable-decoder=flac \
|
--enable-decoder=flac \
|
||||||
" && \
|
" && \
|
||||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||||
git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
|
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
|
||||||
|
cd ffmpeg && \
|
||||||
./configure \
|
./configure \
|
||||||
--libdir=android-libs/armeabi-v7a \
|
--libdir=android-libs/armeabi-v7a \
|
||||||
--arch=arm \
|
--arch=arm \
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
@ -32,6 +37,8 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
|
@ -26,29 +27,27 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and renders audio using FFmpeg.
|
* Decodes and renders audio using FFmpeg.
|
||||||
*/
|
*/
|
||||||
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
|
|
||||||
/**
|
/** The number of input and output buffers. */
|
||||||
* The number of input and output buffers.
|
|
||||||
*/
|
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
/**
|
/** The default input buffer size. */
|
||||||
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
|
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
|
||||||
* insufficient.
|
|
||||||
*/
|
|
||||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
|
|
||||||
|
|
||||||
private final boolean enableFloatOutput;
|
private final boolean enableFloatOutput;
|
||||||
|
|
||||||
private FfmpegDecoder decoder;
|
private @MonotonicNonNull FfmpegDecoder decoder;
|
||||||
|
|
||||||
public FfmpegAudioRenderer() {
|
public FfmpegAudioRenderer() {
|
||||||
this(null, null);
|
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,9 +56,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||||
*/
|
*/
|
||||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
public FfmpegAudioRenderer(
|
||||||
|
@Nullable Handler eventHandler,
|
||||||
|
@Nullable AudioRendererEventListener eventListener,
|
||||||
AudioProcessor... audioProcessors) {
|
AudioProcessor... audioProcessors) {
|
||||||
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
|
this(
|
||||||
|
eventHandler,
|
||||||
|
eventListener,
|
||||||
|
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
|
||||||
|
/* enableFloatOutput= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,8 +77,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
|
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
|
||||||
* adjustment.
|
* adjustment.
|
||||||
*/
|
*/
|
||||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
public FfmpegAudioRenderer(
|
||||||
AudioSink audioSink, boolean enableFloatOutput) {
|
@Nullable Handler eventHandler,
|
||||||
|
@Nullable AudioRendererEventListener eventListener,
|
||||||
|
AudioSink audioSink,
|
||||||
|
boolean enableFloatOutput) {
|
||||||
super(
|
super(
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
|
|
@ -86,10 +94,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||||
Format format) {
|
Format format) {
|
||||||
String sampleMimeType = format.sampleMimeType;
|
Assertions.checkNotNull(format.sampleMimeType);
|
||||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
|
if (!FfmpegLibrary.isAvailable()) {
|
||||||
return FORMAT_UNSUPPORTED_TYPE;
|
return FORMAT_UNSUPPORTED_TYPE;
|
||||||
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
|
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|
||||||
|
|| !isOutputSupported(format)) {
|
||||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||||
return FORMAT_UNSUPPORTED_DRM;
|
return FORMAT_UNSUPPORTED_DRM;
|
||||||
|
|
@ -106,18 +115,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
throws FfmpegDecoderException {
|
throws FfmpegDecoderException {
|
||||||
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
int initialInputBufferSize =
|
||||||
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
|
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||||
|
decoder =
|
||||||
|
new FfmpegDecoder(
|
||||||
|
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Format getOutputFormat() {
|
public Format getOutputFormat() {
|
||||||
|
Assertions.checkNotNull(decoder);
|
||||||
int channelCount = decoder.getChannelCount();
|
int channelCount = decoder.getChannelCount();
|
||||||
int sampleRate = decoder.getSampleRate();
|
int sampleRate = decoder.getSampleRate();
|
||||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
@C.PcmEncoding int encoding = decoder.getEncoding();
|
||||||
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
|
return Format.createAudioSampleFormat(
|
||||||
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
|
/* id= */ null,
|
||||||
|
MimeTypes.AUDIO_RAW,
|
||||||
|
/* codecs= */ null,
|
||||||
|
Format.NO_VALUE,
|
||||||
|
Format.NO_VALUE,
|
||||||
|
channelCount,
|
||||||
|
sampleRate,
|
||||||
|
encoding,
|
||||||
|
Collections.emptyList(),
|
||||||
|
/* drmInitData= */ null,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
/* language= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isOutputSupported(Format inputFormat) {
|
private boolean isOutputSupported(Format inputFormat) {
|
||||||
|
|
@ -125,6 +149,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||||
|
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
||||||
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
@ -30,13 +33,12 @@ import java.util.List;
|
||||||
/* package */ final class FfmpegDecoder extends
|
/* package */ final class FfmpegDecoder extends
|
||||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||||
|
|
||||||
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
|
// Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
|
||||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
|
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
||||||
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
|
|
||||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||||
|
|
||||||
private final String codecName;
|
private final String codecName;
|
||||||
private final byte[] extraData;
|
private final @Nullable byte[] extraData;
|
||||||
private final @C.Encoding int encoding;
|
private final @C.Encoding int encoding;
|
||||||
private final int outputBufferSize;
|
private final int outputBufferSize;
|
||||||
|
|
||||||
|
|
@ -45,18 +47,26 @@ import java.util.List;
|
||||||
private volatile int channelCount;
|
private volatile int channelCount;
|
||||||
private volatile int sampleRate;
|
private volatile int sampleRate;
|
||||||
|
|
||||||
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
public FfmpegDecoder(
|
||||||
String mimeType, List<byte[]> initializationData, boolean outputFloat)
|
int numInputBuffers,
|
||||||
|
int numOutputBuffers,
|
||||||
|
int initialInputBufferSize,
|
||||||
|
Format format,
|
||||||
|
boolean outputFloat)
|
||||||
throws FfmpegDecoderException {
|
throws FfmpegDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||||
if (!FfmpegLibrary.isAvailable()) {
|
if (!FfmpegLibrary.isAvailable()) {
|
||||||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||||
}
|
}
|
||||||
codecName = FfmpegLibrary.getCodecName(mimeType);
|
Assertions.checkNotNull(format.sampleMimeType);
|
||||||
extraData = getExtraData(mimeType, initializationData);
|
codecName =
|
||||||
|
Assertions.checkNotNull(
|
||||||
|
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
|
||||||
|
extraData = getExtraData(format.sampleMimeType, format.initializationData);
|
||||||
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
||||||
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
||||||
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
|
nativeContext =
|
||||||
|
ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
|
||||||
if (nativeContext == 0) {
|
if (nativeContext == 0) {
|
||||||
throw new FfmpegDecoderException("Initialization failed.");
|
throw new FfmpegDecoderException("Initialization failed.");
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +94,7 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FfmpegDecoderException decode(
|
protected @Nullable FfmpegDecoderException decode(
|
||||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
nativeContext = ffmpegReset(nativeContext, extraData);
|
nativeContext = ffmpegReset(nativeContext, extraData);
|
||||||
|
|
@ -103,6 +113,7 @@ import java.util.List;
|
||||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||||
sampleRate = ffmpegGetSampleRate(nativeContext);
|
sampleRate = ffmpegGetSampleRate(nativeContext);
|
||||||
if (sampleRate == 0 && "alac".equals(codecName)) {
|
if (sampleRate == 0 && "alac".equals(codecName)) {
|
||||||
|
Assertions.checkNotNull(extraData);
|
||||||
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
||||||
// See https://trac.ffmpeg.org/ticket/6096
|
// See https://trac.ffmpeg.org/ticket/6096
|
||||||
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
||||||
|
|
@ -148,7 +159,7 @@ import java.util.List;
|
||||||
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
|
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
|
||||||
* not required.
|
* not required.
|
||||||
*/
|
*/
|
||||||
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||||
switch (mimeType) {
|
switch (mimeType) {
|
||||||
case MimeTypes.AUDIO_AAC:
|
case MimeTypes.AUDIO_AAC:
|
||||||
case MimeTypes.AUDIO_ALAC:
|
case MimeTypes.AUDIO_ALAC:
|
||||||
|
|
@ -173,12 +184,20 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
|
private native long ffmpegInitialize(
|
||||||
|
String codecName,
|
||||||
|
@Nullable byte[] extraData,
|
||||||
|
boolean outputFloat,
|
||||||
|
int rawSampleRate,
|
||||||
|
int rawChannelCount);
|
||||||
|
|
||||||
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
||||||
ByteBuffer outputData, int outputSize);
|
ByteBuffer outputData, int outputSize);
|
||||||
private native int ffmpegGetChannelCount(long context);
|
private native int ffmpegGetChannelCount(long context);
|
||||||
private native int ffmpegGetSampleRate(long context);
|
private native int ffmpegGetSampleRate(long context);
|
||||||
private native long ffmpegReset(long context, byte[] extraData);
|
|
||||||
|
private native long ffmpegReset(long context, @Nullable byte[] extraData);
|
||||||
|
|
||||||
private native void ffmpegRelease(long context);
|
private native void ffmpegRelease(long context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
@ -51,10 +53,8 @@ public final class FfmpegLibrary {
|
||||||
return LOADER.isAvailable();
|
return LOADER.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the version of the underlying library if available, or null otherwise. */
|
||||||
* Returns the version of the underlying library if available, or null otherwise.
|
public static @Nullable String getVersion() {
|
||||||
*/
|
|
||||||
public static String getVersion() {
|
|
||||||
return isAvailable() ? ffmpegGetVersion() : null;
|
return isAvailable() ? ffmpegGetVersion() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,19 +62,21 @@ public final class FfmpegLibrary {
|
||||||
* Returns whether the underlying library supports the specified MIME type.
|
* Returns whether the underlying library supports the specified MIME type.
|
||||||
*
|
*
|
||||||
* @param mimeType The MIME type to check.
|
* @param mimeType The MIME type to check.
|
||||||
|
* @param encoding The PCM encoding for raw audio.
|
||||||
*/
|
*/
|
||||||
public static boolean supportsFormat(String mimeType) {
|
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
|
||||||
if (!isAvailable()) {
|
if (!isAvailable()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String codecName = getCodecName(mimeType);
|
String codecName = getCodecName(mimeType, encoding);
|
||||||
return codecName != null && ffmpegHasDecoder(codecName);
|
return codecName != null && ffmpegHasDecoder(codecName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}.
|
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
|
||||||
|
* if it's unsupported.
|
||||||
*/
|
*/
|
||||||
/* package */ static String getCodecName(String mimeType) {
|
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
|
||||||
switch (mimeType) {
|
switch (mimeType) {
|
||||||
case MimeTypes.AUDIO_AAC:
|
case MimeTypes.AUDIO_AAC:
|
||||||
return "aac";
|
return "aac";
|
||||||
|
|
@ -85,6 +87,7 @@ public final class FfmpegLibrary {
|
||||||
case MimeTypes.AUDIO_AC3:
|
case MimeTypes.AUDIO_AC3:
|
||||||
return "ac3";
|
return "ac3";
|
||||||
case MimeTypes.AUDIO_E_AC3:
|
case MimeTypes.AUDIO_E_AC3:
|
||||||
|
case MimeTypes.AUDIO_E_AC3_JOC:
|
||||||
return "eac3";
|
return "eac3";
|
||||||
case MimeTypes.AUDIO_TRUEHD:
|
case MimeTypes.AUDIO_TRUEHD:
|
||||||
return "truehd";
|
return "truehd";
|
||||||
|
|
@ -103,6 +106,14 @@ public final class FfmpegLibrary {
|
||||||
return "flac";
|
return "flac";
|
||||||
case MimeTypes.AUDIO_ALAC:
|
case MimeTypes.AUDIO_ALAC:
|
||||||
return "alac";
|
return "alac";
|
||||||
|
case MimeTypes.AUDIO_RAW:
|
||||||
|
if (encoding == C.ENCODING_PCM_MU_LAW) {
|
||||||
|
return "pcm_mulaw";
|
||||||
|
} else if (encoding == C.ENCODING_PCM_A_LAW) {
|
||||||
|
return "pcm_alaw";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ extern "C" {
|
||||||
#endif
|
#endif
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavresample/avresample.h>
|
#include <libavresample/avresample.h>
|
||||||
|
#include <libavutil/channel_layout.h>
|
||||||
#include <libavutil/error.h>
|
#include <libavutil/error.h>
|
||||||
#include <libavutil/opt.h>
|
#include <libavutil/opt.h>
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
|
||||||
* provided extraData as initialization data for the decoder if it is non-NULL.
|
* provided extraData as initialization data for the decoder if it is non-NULL.
|
||||||
* Returns the created context.
|
* Returns the created context.
|
||||||
*/
|
*/
|
||||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||||
jbyteArray extraData, jboolean outputFloat);
|
jboolean outputFloat, jint rawSampleRate,
|
||||||
|
jint rawChannelCount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the packet into the output buffer, returning the number of bytes
|
* Decodes the packet into the output buffer, returning the number of bytes
|
||||||
|
|
@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||||
jboolean outputFloat) {
|
jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
|
||||||
AVCodec *codec = getCodecByName(env, codecName);
|
AVCodec *codec = getCodecByName(env, codecName);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("Codec not found.");
|
LOGE("Codec not found.");
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
return (jlong) createContext(env, codec, extraData, outputFloat);
|
return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate,
|
||||||
|
rawChannelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||||
|
|
@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||||
LOGE("Unexpected error finding codec %d.", codecId);
|
LOGE("Unexpected error finding codec %d.", codecId);
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
return (jlong) createContext(env, codec, extraData,
|
jboolean outputFloat =
|
||||||
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
(jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
||||||
|
return (jlong)createContext(env, codec, extraData, outputFloat,
|
||||||
|
/* rawSampleRate= */ -1,
|
||||||
|
/* rawChannelCount= */ -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
avcodec_flush_buffers(context);
|
avcodec_flush_buffers(context);
|
||||||
|
|
@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
|
||||||
return codec;
|
return codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||||
jbyteArray extraData, jboolean outputFloat) {
|
jboolean outputFloat, jint rawSampleRate,
|
||||||
|
jint rawChannelCount) {
|
||||||
AVCodecContext *context = avcodec_alloc_context3(codec);
|
AVCodecContext *context = avcodec_alloc_context3(codec);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
LOGE("Failed to allocate context.");
|
LOGE("Failed to allocate context.");
|
||||||
|
|
@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||||
}
|
}
|
||||||
env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);
|
env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);
|
||||||
}
|
}
|
||||||
|
if (context->codec_id == AV_CODEC_ID_PCM_MULAW ||
|
||||||
|
context->codec_id == AV_CODEC_ID_PCM_ALAW) {
|
||||||
|
context->sample_rate = rawSampleRate;
|
||||||
|
context->channels = rawChannelCount;
|
||||||
|
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
||||||
|
}
|
||||||
int result = avcodec_open2(context, codec, NULL);
|
int result = avcodec_open2(context, codec, NULL);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
logError("avcodec_open2", result);
|
logError("avcodec_open2", result);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
||||||
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||||
|
|
||||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||||
assertThat(seeker.hasPendingSeek()).isTrue();
|
assertThat(seeker.isSeeking()).isTrue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||||
implements Runnable {
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
|
@ -33,111 +29,51 @@ import java.nio.ByteBuffer;
|
||||||
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
|
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
|
||||||
* frame that contains the target sample.
|
* frame that contains the target sample.
|
||||||
*/
|
*/
|
||||||
/* package */ final class FlacBinarySearchSeeker {
|
/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
|
||||||
|
|
||||||
/**
|
|
||||||
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
|
|
||||||
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
|
|
||||||
* the new seek position.
|
|
||||||
*/
|
|
||||||
private static final long MAX_SKIP_BYTES = 256 * 1024;
|
|
||||||
|
|
||||||
private final FlacStreamInfo streamInfo;
|
|
||||||
private final FlacBinarySearchSeekMap seekMap;
|
|
||||||
private final FlacDecoderJni decoderJni;
|
private final FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
private final long firstFramePosition;
|
|
||||||
private final long inputLength;
|
|
||||||
private final long approxBytesPerFrame;
|
|
||||||
|
|
||||||
private @Nullable SeekOperationParams pendingSeekOperationParams;
|
|
||||||
|
|
||||||
public FlacBinarySearchSeeker(
|
public FlacBinarySearchSeeker(
|
||||||
FlacStreamInfo streamInfo,
|
FlacStreamInfo streamInfo,
|
||||||
long firstFramePosition,
|
long firstFramePosition,
|
||||||
long inputLength,
|
long inputLength,
|
||||||
FlacDecoderJni decoderJni) {
|
FlacDecoderJni decoderJni) {
|
||||||
this.streamInfo = Assertions.checkNotNull(streamInfo);
|
super(
|
||||||
|
new FlacSeekTimestampConverter(streamInfo),
|
||||||
|
new FlacTimestampSeeker(decoderJni),
|
||||||
|
streamInfo.durationUs(),
|
||||||
|
/* floorTimePosition= */ 0,
|
||||||
|
/* ceilingTimePosition= */ streamInfo.totalSamples,
|
||||||
|
/* floorBytePosition= */ firstFramePosition,
|
||||||
|
/* ceilingBytePosition= */ inputLength,
|
||||||
|
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
|
||||||
|
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
|
||||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||||
this.firstFramePosition = firstFramePosition;
|
|
||||||
this.inputLength = inputLength;
|
|
||||||
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
|
|
||||||
|
|
||||||
pendingSeekOperationParams = null;
|
|
||||||
seekMap =
|
|
||||||
new FlacBinarySearchSeekMap(
|
|
||||||
streamInfo,
|
|
||||||
firstFramePosition,
|
|
||||||
inputLength,
|
|
||||||
streamInfo.durationUs(),
|
|
||||||
approxBytesPerFrame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the seek map for the wrapped FLAC stream. */
|
@Override
|
||||||
public SeekMap getSeekMap() {
|
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
|
||||||
return seekMap;
|
if (!foundTargetFrame) {
|
||||||
|
// If we can't find the target frame (sample), we need to reset the decoder jni so that
|
||||||
|
// it can continue from the result position.
|
||||||
|
decoderJni.reset(resultPosition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the target time in microseconds within the stream to seek to. */
|
private static final class FlacTimestampSeeker implements TimestampSeeker {
|
||||||
public void setSeekTargetUs(long timeUs) {
|
|
||||||
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
|
private final FlacDecoderJni decoderJni;
|
||||||
return;
|
|
||||||
|
private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
|
||||||
|
this.decoderJni = decoderJni;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingSeekOperationParams =
|
@Override
|
||||||
new SeekOperationParams(
|
public TimestampSearchResult searchForTimestamp(
|
||||||
timeUs,
|
ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
|
||||||
streamInfo.getSampleIndex(timeUs),
|
throws IOException, InterruptedException {
|
||||||
/* floorSample= */ 0,
|
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
||||||
/* ceilingSample= */ streamInfo.totalSamples,
|
long searchPosition = input.getPosition();
|
||||||
/* floorPosition= */ firstFramePosition,
|
|
||||||
/* ceilingPosition= */ inputLength,
|
|
||||||
approxBytesPerFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
|
|
||||||
public boolean hasPendingSeek() {
|
|
||||||
return pendingSeekOperationParams != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
|
|
||||||
* {@link Extractor}.
|
|
||||||
*
|
|
||||||
* @param input The {@link ExtractorInput} from which data should be read.
|
|
||||||
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
|
|
||||||
* to hold the position of the required seek.
|
|
||||||
* @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
|
|
||||||
* updated to hold the extracted frame that contains the target sample. The caller needs to
|
|
||||||
* check the byte buffer limit to see if an extracted frame is available.
|
|
||||||
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
|
|
||||||
* @throws IOException If an error occurred reading from the input.
|
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
|
||||||
*/
|
|
||||||
public int handlePendingSeek(
|
|
||||||
ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
|
|
||||||
throws InterruptedException, IOException {
|
|
||||||
outputBuffer.position(0);
|
|
||||||
outputBuffer.limit(0);
|
|
||||||
while (true) {
|
|
||||||
long floorPosition = pendingSeekOperationParams.floorPosition;
|
|
||||||
long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
|
|
||||||
long searchPosition = pendingSeekOperationParams.nextSearchPosition;
|
|
||||||
|
|
||||||
// streamInfo may not contain minFrameSize, in which case this value will be 0.
|
|
||||||
int minFrameSize = Math.max(1, streamInfo.minFrameSize);
|
|
||||||
if (floorPosition + minFrameSize >= ceilingPosition) {
|
|
||||||
// The seeking range is too small for more than 1 frame, so we can just continue from
|
|
||||||
// the floor position.
|
|
||||||
pendingSeekOperationParams = null;
|
|
||||||
decoderJni.reset(floorPosition);
|
|
||||||
return seekToPosition(input, floorPosition, seekPositionHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipInputUntilPosition(input, searchPosition)) {
|
|
||||||
return seekToPosition(input, searchPosition, seekPositionHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
decoderJni.reset(searchPosition);
|
decoderJni.reset(searchPosition);
|
||||||
try {
|
try {
|
||||||
decoderJni.decodeSampleWithBacktrackPosition(
|
decoderJni.decodeSampleWithBacktrackPosition(
|
||||||
|
|
@ -145,11 +81,10 @@ import java.nio.ByteBuffer;
|
||||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||||
// For some reasons, the extractor can't find a frame mid-stream.
|
// For some reasons, the extractor can't find a frame mid-stream.
|
||||||
// Stop the seeking and let it re-try playing at the last search position.
|
// Stop the seeking and let it re-try playing at the last search position.
|
||||||
pendingSeekOperationParams = null;
|
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||||
throw new IOException("Cannot read frame at position " + searchPosition, e);
|
|
||||||
}
|
}
|
||||||
if (outputBuffer.limit() == 0) {
|
if (outputBuffer.limit() == 0) {
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
||||||
|
|
@ -157,184 +92,35 @@ import java.nio.ByteBuffer;
|
||||||
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
||||||
|
|
||||||
boolean targetSampleInLastFrame =
|
boolean targetSampleInLastFrame =
|
||||||
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
|
lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;
|
||||||
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
|
|
||||||
|
|
||||||
if (targetSampleInLastFrame) {
|
if (targetSampleInLastFrame) {
|
||||||
pendingSeekOperationParams = null;
|
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
||||||
return Extractor.RESULT_CONTINUE;
|
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
||||||
}
|
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
||||||
|
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
||||||
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
|
return TimestampSearchResult.underestimatedResult(
|
||||||
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
|
nextFrameSampleIndex, nextFrameSamplePosition);
|
||||||
} else {
|
} else {
|
||||||
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
|
return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean skipInputUntilPosition(ExtractorInput input, long position)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
long bytesToSkip = position - input.getPosition();
|
|
||||||
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
|
|
||||||
input.skipFully((int) bytesToSkip);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int seekToPosition(
|
|
||||||
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
|
|
||||||
if (position == input.getPosition()) {
|
|
||||||
return Extractor.RESULT_CONTINUE;
|
|
||||||
} else {
|
|
||||||
seekPositionHolder.position = position;
|
|
||||||
return Extractor.RESULT_SEEK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
|
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
|
||||||
*
|
* the timestamp for a stream seek time position.
|
||||||
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
|
|
||||||
* [floorPosition, ceilingPosition).
|
|
||||||
*/
|
*/
|
||||||
private static final class SeekOperationParams {
|
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
||||||
private final long seekTimeUs;
|
|
||||||
private final long targetSample;
|
|
||||||
private final long approxBytesPerFrame;
|
|
||||||
private long floorSample;
|
|
||||||
private long ceilingSample;
|
|
||||||
private long floorPosition;
|
|
||||||
private long ceilingPosition;
|
|
||||||
private long nextSearchPosition;
|
|
||||||
|
|
||||||
private SeekOperationParams(
|
|
||||||
long seekTimeUs,
|
|
||||||
long targetSample,
|
|
||||||
long floorSample,
|
|
||||||
long ceilingSample,
|
|
||||||
long floorPosition,
|
|
||||||
long ceilingPosition,
|
|
||||||
long approxBytesPerFrame) {
|
|
||||||
this.seekTimeUs = seekTimeUs;
|
|
||||||
this.floorSample = floorSample;
|
|
||||||
this.ceilingSample = ceilingSample;
|
|
||||||
this.floorPosition = floorPosition;
|
|
||||||
this.ceilingPosition = ceilingPosition;
|
|
||||||
this.targetSample = targetSample;
|
|
||||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
|
||||||
updateNextSearchPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Updates the floor constraints (inclusive) of the seek operation. */
|
|
||||||
private void updateSeekFloor(long floorSample, long floorPosition) {
|
|
||||||
this.floorSample = floorSample;
|
|
||||||
this.floorPosition = floorPosition;
|
|
||||||
updateNextSearchPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Updates the ceiling constraints (exclusive) of the seek operation. */
|
|
||||||
private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
|
|
||||||
this.ceilingSample = ceilingSample;
|
|
||||||
this.ceilingPosition = ceilingPosition;
|
|
||||||
updateNextSearchPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNextSearchPosition() {
|
|
||||||
this.nextSearchPosition =
|
|
||||||
getNextSearchPosition(
|
|
||||||
targetSample,
|
|
||||||
floorSample,
|
|
||||||
ceilingSample,
|
|
||||||
floorPosition,
|
|
||||||
ceilingPosition,
|
|
||||||
approxBytesPerFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next position in FLAC stream to search for target sample, given [floorPosition,
|
|
||||||
* ceilingPosition).
|
|
||||||
*/
|
|
||||||
private static long getNextSearchPosition(
|
|
||||||
long targetSample,
|
|
||||||
long floorSample,
|
|
||||||
long ceilingSample,
|
|
||||||
long floorPosition,
|
|
||||||
long ceilingPosition,
|
|
||||||
long approxBytesPerFrame) {
|
|
||||||
if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
|
|
||||||
return floorPosition;
|
|
||||||
}
|
|
||||||
long samplesToSkip = targetSample - floorSample;
|
|
||||||
long estimatedBytesPerSample =
|
|
||||||
Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
|
|
||||||
// In the stream, the samples are accessed in a group of frame. Given a stream position, the
|
|
||||||
// seeker will be able to find the first frame following that position.
|
|
||||||
// Hence, if our target sample is in the middle of a frame, and our estimate position is
|
|
||||||
// correct, or very near the actual sample position, the seeker will keep accessing the next
|
|
||||||
// frame, rather than the frame that contains the target sample.
|
|
||||||
// Moreover, it's better to under-estimate rather than over-estimate, because the extractor
|
|
||||||
// input can skip forward easily, but cannot rewind easily (it may require a new connection
|
|
||||||
// to be made).
|
|
||||||
// Therefore, we should reduce the estimated position by some amount, so it will converge to
|
|
||||||
// the correct frame earlier.
|
|
||||||
long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
|
|
||||||
long confidenceInterval = bytesToSkip / 20;
|
|
||||||
|
|
||||||
long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
|
|
||||||
long estimatedPosition = estimatedFramePosition - confidenceInterval;
|
|
||||||
|
|
||||||
return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
|
|
||||||
* SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
|
|
||||||
* #getSeekPoints(long)} query.
|
|
||||||
*/
|
|
||||||
private static final class FlacBinarySearchSeekMap implements SeekMap {
|
|
||||||
private final FlacStreamInfo streamInfo;
|
private final FlacStreamInfo streamInfo;
|
||||||
private final long firstFramePosition;
|
|
||||||
private final long inputLength;
|
|
||||||
private final long approxBytesPerFrame;
|
|
||||||
private final long durationUs;
|
|
||||||
|
|
||||||
private FlacBinarySearchSeekMap(
|
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
|
||||||
FlacStreamInfo streamInfo,
|
|
||||||
long firstFramePosition,
|
|
||||||
long inputLength,
|
|
||||||
long durationUs,
|
|
||||||
long approxBytesPerFrame) {
|
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
this.firstFramePosition = firstFramePosition;
|
|
||||||
this.inputLength = inputLength;
|
|
||||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
|
||||||
this.durationUs = durationUs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSeekable() {
|
public long timeUsToTargetTime(long timeUs) {
|
||||||
return true;
|
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekPoints getSeekPoints(long timeUs) {
|
|
||||||
long nextSearchPosition =
|
|
||||||
SeekOperationParams.getNextSearchPosition(
|
|
||||||
streamInfo.getSampleIndex(timeUs),
|
|
||||||
/* floorSample= */ 0,
|
|
||||||
/* ceilingSample= */ streamInfo.totalSamples,
|
|
||||||
/* floorPosition= */ firstFramePosition,
|
|
||||||
/* ceilingPosition= */ inputLength,
|
|
||||||
approxBytesPerFrame);
|
|
||||||
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDurationUs() {
|
|
||||||
return durationUs;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||||
|
|
@ -37,11 +38,17 @@ import java.util.List;
|
||||||
*
|
*
|
||||||
* @param numInputBuffers The number of input buffers.
|
* @param numInputBuffers The number of input buffers.
|
||||||
* @param numOutputBuffers The number of output buffers.
|
* @param numOutputBuffers The number of output buffers.
|
||||||
|
* @param maxInputBufferSize The maximum required input buffer size if known, or {@link
|
||||||
|
* Format#NO_VALUE} otherwise.
|
||||||
* @param initializationData Codec-specific initialization data. It should contain only one entry
|
* @param initializationData Codec-specific initialization data. It should contain only one entry
|
||||||
* which is the flac file header.
|
* which is the flac file header.
|
||||||
* @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public FlacDecoder(int numInputBuffers, int numOutputBuffers, List<byte[]> initializationData)
|
public FlacDecoder(
|
||||||
|
int numInputBuffers,
|
||||||
|
int numOutputBuffers,
|
||||||
|
int maxInputBufferSize,
|
||||||
|
List<byte[]> initializationData)
|
||||||
throws FlacDecoderException {
|
throws FlacDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||||
if (initializationData.size() != 1) {
|
if (initializationData.size() != 1) {
|
||||||
|
|
@ -60,7 +67,9 @@ import java.util.List;
|
||||||
throw new FlacDecoderException("Metadata decoding failed");
|
throw new FlacDecoderException("Metadata decoding failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialInputBufferSize(streamInfo.maxFrameSize);
|
int initialInputBufferSize =
|
||||||
|
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
|
||||||
|
setInitialInputBufferSize(initialInputBufferSize);
|
||||||
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
|
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
|
||||||
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.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
|
|
@ -46,24 +47,14 @@ import java.util.Arrays;
|
||||||
*/
|
*/
|
||||||
public final class FlacExtractor implements Extractor {
|
public final class FlacExtractor implements Extractor {
|
||||||
|
|
||||||
/**
|
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
|
||||||
* Factory that returns one extractor which is a {@link FlacExtractor}.
|
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
|
||||||
*/
|
|
||||||
public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Extractor[] createExtractors() {
|
|
||||||
return new Extractor[] {new FlacExtractor()};
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Flags controlling the behavior of the extractor. */
|
/** Flags controlling the behavior of the extractor. */
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef(
|
@IntDef(
|
||||||
flag = true,
|
flag = true,
|
||||||
value = {FLAG_DISABLE_ID3_METADATA}
|
value = {FLAG_DISABLE_ID3_METADATA})
|
||||||
)
|
|
||||||
public @interface Flags {}
|
public @interface Flags {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,6 +79,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
|
|
||||||
private ParsableByteArray outputBuffer;
|
private ParsableByteArray outputBuffer;
|
||||||
private ByteBuffer outputByteBuffer;
|
private ByteBuffer outputByteBuffer;
|
||||||
|
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
|
||||||
private FlacStreamInfo streamInfo;
|
private FlacStreamInfo streamInfo;
|
||||||
|
|
||||||
private Metadata id3Metadata;
|
private Metadata id3Metadata;
|
||||||
|
|
@ -140,7 +132,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
decoderJni.setData(input);
|
decoderJni.setData(input);
|
||||||
readPastStreamInfo(input);
|
readPastStreamInfo(input);
|
||||||
|
|
||||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
|
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
|
||||||
return handlePendingSeek(input, seekPosition);
|
return handlePendingSeek(input, seekPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,6 +216,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
outputFormat(streamInfo);
|
outputFormat(streamInfo);
|
||||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
||||||
|
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
||||||
|
|
@ -286,9 +279,10 @@ public final class FlacExtractor implements Extractor {
|
||||||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws InterruptedException, IOException {
|
throws InterruptedException, IOException {
|
||||||
int seekResult =
|
int seekResult =
|
||||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
|
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
||||||
|
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||||
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
|
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
|
||||||
}
|
}
|
||||||
return seekResult;
|
return seekResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
throws FlacDecoderException {
|
throws FlacDecoderException {
|
||||||
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
|
return new FlacDecoder(
|
||||||
|
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
extensions/flac/src/test/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest package="com.google.android.exoplayer2.ext.flac"/>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
||||||
|
/** Unit test for {@link DefaultExtractorsFactory}. */
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public final class DefaultExtractorsFactoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateExtractors_returnExpectedClasses() {
|
||||||
|
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
|
||||||
|
|
||||||
|
Extractor[] extractors = defaultExtractorsFactory.createExtractors();
|
||||||
|
List<Class> listCreatedExtractorClasses = new ArrayList<>();
|
||||||
|
for (Extractor extractor : extractors) {
|
||||||
|
listCreatedExtractorClasses.add(extractor.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
Class[] expectedExtractorClassses =
|
||||||
|
new Class[] {
|
||||||
|
MatroskaExtractor.class,
|
||||||
|
FragmentedMp4Extractor.class,
|
||||||
|
Mp4Extractor.class,
|
||||||
|
Mp3Extractor.class,
|
||||||
|
AdtsExtractor.class,
|
||||||
|
Ac3Extractor.class,
|
||||||
|
TsExtractor.class,
|
||||||
|
FlvExtractor.class,
|
||||||
|
OggExtractor.class,
|
||||||
|
PsExtractor.class,
|
||||||
|
WavExtractor.class,
|
||||||
|
AmrExtractor.class,
|
||||||
|
FlacExtractor.class
|
||||||
|
};
|
||||||
|
|
||||||
|
assertThat(listCreatedExtractorClasses).containsNoDuplicates();
|
||||||
|
assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.vr.sdk.audio.GvrAudioSurround;
|
import com.google.vr.sdk.audio.GvrAudioSurround;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||||
@Override
|
@Override
|
||||||
public void queueInput(ByteBuffer input) {
|
public void queueInput(ByteBuffer input) {
|
||||||
int position = input.position();
|
int position = input.position();
|
||||||
|
Assertions.checkNotNull(gvrAudioSurround);
|
||||||
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
|
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
|
||||||
input.position(position + readBytes);
|
input.position(position + readBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueEndOfStream() {
|
public void queueEndOfStream() {
|
||||||
|
Assertions.checkNotNull(gvrAudioSurround);
|
||||||
inputEnded = true;
|
inputEnded = true;
|
||||||
gvrAudioSurround.triggerProcessing();
|
gvrAudioSurround.triggerProcessing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer getOutput() {
|
public ByteBuffer getOutput() {
|
||||||
|
Assertions.checkNotNull(gvrAudioSurround);
|
||||||
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
||||||
buffer.position(0).limit(writtenBytes);
|
buffer.position(0).limit(writtenBytes);
|
||||||
return buffer;
|
return buffer;
|
||||||
|
|
@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnded() {
|
public boolean isEnded() {
|
||||||
|
Assertions.checkNotNull(gvrAudioSurround);
|
||||||
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
|
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
@ -26,19 +31,25 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// This dependency is necessary to force the supportLibraryVersion of
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4'
|
||||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
|
||||||
// is included via:
|
|
||||||
// com.google.android.gms:play-services-ads:12.0.0
|
|
||||||
// |-- com.google.android.gms:play-services-ads-lite:12.0.0
|
|
||||||
// |-- com.google.android.gms:play-services-basement:12.0.0
|
|
||||||
// |-- com.android.support:support-v4:26.1.0
|
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
implementation 'com.google.android.gms:play-services-ads:15.0.1'
|
||||||
|
// These dependencies are necessary to force the supportLibraryVersion of
|
||||||
|
// com.android.support:support-v4 and com.android.support:customtabs to be
|
||||||
|
// used. Else older versions are used, for example via:
|
||||||
|
// com.google.android.gms:play-services-ads:15.0.1
|
||||||
|
// |-- com.android.support:customtabs:26.1.0
|
||||||
|
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
||||||
|
implementation 'com.android.support:customtabs:' + supportLibraryVersion
|
||||||
|
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
|
testImplementation 'junit:junit:' + junitVersion
|
||||||
|
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
javadocTitle = 'IMA extension'
|
javadocTitle = 'IMA extension'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.webkit.WebView;
|
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdError;
|
import com.google.ads.interactivemedia.v3.api.AdError;
|
||||||
|
|
@ -38,6 +37,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;
|
||||||
|
|
@ -53,6 +53,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||||
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
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.MimeTypes;
|
||||||
|
|
@ -62,15 +63,20 @@ 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;
|
||||||
|
|
||||||
/**
|
/** Loads ads using the IMA SDK. All methods are called on the main thread. */
|
||||||
* Loads ads using the IMA SDK. All methods are called on the main thread.
|
public final class ImaAdsLoader
|
||||||
*/
|
implements Player.EventListener,
|
||||||
public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader,
|
AdsLoader,
|
||||||
VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
|
VideoAdPlayer,
|
||||||
|
ContentProgressProvider,
|
||||||
|
AdErrorListener,
|
||||||
|
AdsLoadedListener,
|
||||||
|
AdEventListener {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
||||||
|
|
@ -85,6 +91,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
private @Nullable AdEventListener adEventListener;
|
private @Nullable AdEventListener adEventListener;
|
||||||
private int vastLoadTimeoutMs;
|
private int vastLoadTimeoutMs;
|
||||||
private int mediaLoadTimeoutMs;
|
private int mediaLoadTimeoutMs;
|
||||||
|
private ImaFactory imaFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder for {@link ImaAdsLoader}.
|
* Creates a new builder for {@link ImaAdsLoader}.
|
||||||
|
|
@ -95,6 +102,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
this.context = Assertions.checkNotNull(context);
|
this.context = Assertions.checkNotNull(context);
|
||||||
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
|
imaFactory = new DefaultImaFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -149,6 +157,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @VisibleForTesting
|
||||||
|
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
|
||||||
|
this.imaFactory = Assertions.checkNotNull(imaFactory);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
|
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
|
||||||
*
|
*
|
||||||
|
|
@ -165,7 +179,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
null,
|
null,
|
||||||
vastLoadTimeoutMs,
|
vastLoadTimeoutMs,
|
||||||
mediaLoadTimeoutMs,
|
mediaLoadTimeoutMs,
|
||||||
adEventListener);
|
adEventListener,
|
||||||
|
imaFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,7 +198,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
adsResponse,
|
adsResponse,
|
||||||
vastLoadTimeoutMs,
|
vastLoadTimeoutMs,
|
||||||
mediaLoadTimeoutMs,
|
mediaLoadTimeoutMs,
|
||||||
adEventListener);
|
adEventListener,
|
||||||
|
imaFactory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,14 +226,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
|
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
|
||||||
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
|
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
|
||||||
|
|
||||||
/**
|
|
||||||
* The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
|
|
||||||
* clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
|
|
||||||
* the WebView directly when an ad starts. See [Internal: b/62371030].
|
|
||||||
*/
|
|
||||||
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
|
|
||||||
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
|
||||||
|
|
||||||
private static final int TIMEOUT_UNSET = -1;
|
private static final int TIMEOUT_UNSET = -1;
|
||||||
|
|
||||||
/** The state of ad playback. */
|
/** The state of ad playback. */
|
||||||
|
|
@ -242,9 +250,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
private final int vastLoadTimeoutMs;
|
private final int vastLoadTimeoutMs;
|
||||||
private final int mediaLoadTimeoutMs;
|
private final int mediaLoadTimeoutMs;
|
||||||
private final @Nullable AdEventListener adEventListener;
|
private final @Nullable AdEventListener adEventListener;
|
||||||
|
private final ImaFactory imaFactory;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final List<VideoAdPlayerCallback> adCallbacks;
|
private final List<VideoAdPlayerCallback> adCallbacks;
|
||||||
private final ImaSdkFactory imaSdkFactory;
|
|
||||||
private final AdDisplayContainer adDisplayContainer;
|
private final AdDisplayContainer adDisplayContainer;
|
||||||
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
||||||
|
|
||||||
|
|
@ -252,9 +260,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
private List<String> supportedMimeTypes;
|
private List<String> supportedMimeTypes;
|
||||||
private EventListener eventListener;
|
private EventListener eventListener;
|
||||||
private Player player;
|
private Player player;
|
||||||
private ViewGroup adUiViewGroup;
|
|
||||||
private VideoProgressUpdate lastContentProgress;
|
private VideoProgressUpdate lastContentProgress;
|
||||||
private VideoProgressUpdate lastAdProgress;
|
private VideoProgressUpdate lastAdProgress;
|
||||||
|
private int lastVolumePercentage;
|
||||||
|
|
||||||
private AdsManager adsManager;
|
private AdsManager adsManager;
|
||||||
private AdLoadException pendingAdLoadError;
|
private AdLoadException pendingAdLoadError;
|
||||||
|
|
@ -267,13 +275,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 +289,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 +312,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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -337,7 +335,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
/* adsResponse= */ null,
|
/* adsResponse= */ null,
|
||||||
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
/* adEventListener= */ null);
|
/* adEventListener= */ null,
|
||||||
|
/* imaFactory= */ new DefaultImaFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -360,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
/* adsResponse= */ null,
|
/* adsResponse= */ null,
|
||||||
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||||
/* adEventListener= */ null);
|
/* adEventListener= */ null,
|
||||||
|
/* imaFactory= */ new DefaultImaFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImaAdsLoader(
|
private ImaAdsLoader(
|
||||||
|
|
@ -370,26 +370,30 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
@Nullable String adsResponse,
|
@Nullable String adsResponse,
|
||||||
int vastLoadTimeoutMs,
|
int vastLoadTimeoutMs,
|
||||||
int mediaLoadTimeoutMs,
|
int mediaLoadTimeoutMs,
|
||||||
@Nullable AdEventListener adEventListener) {
|
@Nullable AdEventListener adEventListener,
|
||||||
|
ImaFactory imaFactory) {
|
||||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||||
this.adTagUri = adTagUri;
|
this.adTagUri = adTagUri;
|
||||||
this.adsResponse = adsResponse;
|
this.adsResponse = adsResponse;
|
||||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||||
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||||
this.adEventListener = adEventListener;
|
this.adEventListener = adEventListener;
|
||||||
period = new Timeline.Period();
|
this.imaFactory = imaFactory;
|
||||||
adCallbacks = new ArrayList<>(1);
|
|
||||||
imaSdkFactory = ImaSdkFactory.getInstance();
|
|
||||||
adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
|
|
||||||
adDisplayContainer.setPlayer(this);
|
|
||||||
if (imaSdkSettings == null) {
|
if (imaSdkSettings == null) {
|
||||||
imaSdkSettings = imaSdkFactory.createImaSdkSettings();
|
imaSdkSettings = imaFactory.createImaSdkSettings();
|
||||||
|
if (DEBUG) {
|
||||||
|
imaSdkSettings.setDebugMode(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
||||||
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
||||||
adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
|
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
|
||||||
adsLoader.addAdErrorListener(this);
|
period = new Timeline.Period();
|
||||||
adsLoader.addAdsLoadedListener(this);
|
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
||||||
|
adDisplayContainer = imaFactory.createAdDisplayContainer();
|
||||||
|
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
|
||||||
|
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
|
||||||
|
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||||
pendingContentPositionMs = C.TIME_UNSET;
|
pendingContentPositionMs = C.TIME_UNSET;
|
||||||
|
|
@ -405,6 +409,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.
|
||||||
*
|
*
|
||||||
|
|
@ -421,7 +436,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
}
|
}
|
||||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||||
pendingAdRequestContext = new Object();
|
pendingAdRequestContext = new Object();
|
||||||
AdsRequest request = imaSdkFactory.createAdsRequest();
|
AdsRequest request = imaFactory.createAdsRequest();
|
||||||
if (adTagUri != null) {
|
if (adTagUri != null) {
|
||||||
request.setAdTagUrl(adTagUri.toString());
|
request.setAdTagUrl(adTagUri.toString());
|
||||||
} else /* adsResponse != null */ {
|
} else /* adsResponse != null */ {
|
||||||
|
|
@ -447,9 +462,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
} else if (contentType == C.TYPE_HLS) {
|
} else if (contentType == C.TYPE_HLS) {
|
||||||
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
||||||
} else if (contentType == C.TYPE_OTHER) {
|
} else if (contentType == C.TYPE_OTHER) {
|
||||||
supportedMimeTypes.addAll(Arrays.asList(
|
supportedMimeTypes.addAll(
|
||||||
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
|
Arrays.asList(
|
||||||
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
|
MimeTypes.VIDEO_MP4,
|
||||||
|
MimeTypes.VIDEO_WEBM,
|
||||||
|
MimeTypes.VIDEO_H263,
|
||||||
|
MimeTypes.AUDIO_MP4,
|
||||||
|
MimeTypes.AUDIO_MPEG));
|
||||||
} else if (contentType == C.TYPE_SS) {
|
} else if (contentType == C.TYPE_SS) {
|
||||||
// IMA does not support Smooth Streaming ad media.
|
// IMA does not support Smooth Streaming ad media.
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +480,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
|
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.adUiViewGroup = adUiViewGroup;
|
lastVolumePercentage = 0;
|
||||||
lastAdProgress = null;
|
lastAdProgress = null;
|
||||||
lastContentProgress = null;
|
lastContentProgress = null;
|
||||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||||
|
|
@ -490,12 +509,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
||||||
adsManager.pause();
|
adsManager.pause();
|
||||||
}
|
}
|
||||||
|
lastVolumePercentage = getVolume();
|
||||||
lastAdProgress = getAdProgress();
|
lastAdProgress = getAdProgress();
|
||||||
lastContentProgress = getContentProgress();
|
lastContentProgress = getContentProgress();
|
||||||
player.removeListener(this);
|
player.removeListener(this);
|
||||||
player = null;
|
player = null;
|
||||||
eventListener = null;
|
eventListener = null;
|
||||||
adUiViewGroup = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -505,6 +524,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
|
||||||
|
|
@ -554,7 +578,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 {
|
||||||
|
|
@ -607,8 +631,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
|
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
|
||||||
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
|
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
|
||||||
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
||||||
expectedAdGroupIndex =
|
int adGroupIndexForPosition =
|
||||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||||
|
if (adGroupIndexForPosition != C.INDEX_UNSET) {
|
||||||
|
expectedAdGroupIndex = adGroupIndexForPosition;
|
||||||
|
}
|
||||||
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
|
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
|
||||||
contentPositionMs = player.getCurrentPosition();
|
contentPositionMs = player.getCurrentPosition();
|
||||||
// Update the expected ad group index for the current content position. The update is delayed
|
// Update the expected ad group index for the current content position. The update is delayed
|
||||||
|
|
@ -647,9 +674,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVolume() {
|
||||||
|
if (player == null) {
|
||||||
|
return lastVolumePercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.AudioComponent audioComponent = player.getAudioComponent();
|
||||||
|
if (audioComponent != null) {
|
||||||
|
return (int) (audioComponent.getVolume() * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a selected track using an audio renderer.
|
||||||
|
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
|
||||||
|
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
|
||||||
|
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@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,
|
||||||
|
|
@ -658,9 +713,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");
|
||||||
|
|
@ -689,6 +741,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.
|
||||||
|
|
@ -732,6 +788,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");
|
||||||
|
|
@ -771,8 +831,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;
|
||||||
|
|
@ -861,8 +921,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private void startAdPlayback() {
|
private void startAdPlayback() {
|
||||||
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
|
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
|
||||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
|
||||||
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
||||||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
||||||
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||||
|
|
@ -951,11 +1010,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
imaPausedContent = true;
|
imaPausedContent = true;
|
||||||
pauseContentInternal();
|
pauseContentInternal();
|
||||||
break;
|
break;
|
||||||
case STARTED:
|
|
||||||
if (ad.isSkippable()) {
|
|
||||||
focusSkipButton();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TAPPED:
|
case TAPPED:
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onAdTapped();
|
eventListener.onAdTapped();
|
||||||
|
|
@ -978,6 +1032,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
handleAdGroupLoadError(new IOException(message));
|
handleAdGroupLoadError(new IOException(message));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case STARTED:
|
||||||
case ALL_ADS_COMPLETED:
|
case ALL_ADS_COMPLETED:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
@ -1072,6 +1127,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
if (pendingAdLoadError == null) {
|
if (pendingAdLoadError == null) {
|
||||||
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
|
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
|
||||||
}
|
}
|
||||||
|
// Discard the ad break, which makes sure we don't receive duplicate load error events.
|
||||||
|
adsManager.discardAdBreak();
|
||||||
|
// Set the next expected ad group index so we can handle multiple load errors in a row.
|
||||||
|
adGroupIndex++;
|
||||||
|
if (adGroupIndex < adPlaybackState.adGroupCount) {
|
||||||
|
expectedAdGroupIndex = adGroupIndex;
|
||||||
|
} else {
|
||||||
|
expectedAdGroupIndex = C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
pendingContentPositionMs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
||||||
|
|
@ -1079,6 +1144,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.
|
||||||
|
|
@ -1125,15 +1194,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void focusSkipButton() {
|
|
||||||
if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0
|
|
||||||
&& adUiViewGroup.getChildAt(0) instanceof WebView) {
|
|
||||||
WebView webView = (WebView) (adUiViewGroup.getChildAt(0));
|
|
||||||
webView.requestFocus();
|
|
||||||
webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
|
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
|
||||||
* ads in the ad group have loaded.
|
* ads in the ad group have loaded.
|
||||||
|
|
@ -1161,7 +1221,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);
|
||||||
|
|
@ -1214,4 +1274,49 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Factory for objects provided by the IMA SDK. */
|
||||||
|
// @VisibleForTesting
|
||||||
|
/* package */ interface ImaFactory {
|
||||||
|
/** @see ImaSdkSettings */
|
||||||
|
ImaSdkSettings createImaSdkSettings();
|
||||||
|
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */
|
||||||
|
AdsRenderingSettings createAdsRenderingSettings();
|
||||||
|
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */
|
||||||
|
AdDisplayContainer createAdDisplayContainer();
|
||||||
|
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
|
||||||
|
AdsRequest createAdsRequest();
|
||||||
|
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
|
||||||
|
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||||
|
Context context, ImaSdkSettings imaSdkSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
|
||||||
|
private static final class DefaultImaFactory implements ImaFactory {
|
||||||
|
@Override
|
||||||
|
public ImaSdkSettings createImaSdkSettings() {
|
||||||
|
return ImaSdkFactory.getInstance().createImaSdkSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdsRenderingSettings createAdsRenderingSettings() {
|
||||||
|
return ImaSdkFactory.getInstance().createAdsRenderingSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdDisplayContainer createAdDisplayContainer() {
|
||||||
|
return ImaSdkFactory.getInstance().createAdDisplayContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdsRequest createAdsRequest() {
|
||||||
|
return ImaSdkFactory.getInstance().createAdsRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||||
|
Context context, ImaSdkSettings imaSdkSettings) {
|
||||||
|
return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,12 +36,10 @@ import java.io.IOException;
|
||||||
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final class ImaAdsMediaSource extends BaseMediaSource {
|
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
|
||||||
|
|
||||||
private final AdsMediaSource adsMediaSource;
|
private final AdsMediaSource adsMediaSource;
|
||||||
|
|
||||||
private SourceInfoRefreshListener adsMediaSourceListener;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new source that inserts ads linearly with the content specified by
|
* Constructs a new source that inserts ads linearly with the content specified by
|
||||||
* {@code contentMediaSource}.
|
* {@code contentMediaSource}.
|
||||||
|
|
@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
|
public void prepareSourceInternal(
|
||||||
adsMediaSourceListener =
|
final ExoPlayer player,
|
||||||
new SourceInfoRefreshListener() {
|
boolean isTopLevelSource,
|
||||||
@Override
|
@Nullable TransferListener mediaTransferListener) {
|
||||||
public void onSourceInfoRefreshed(
|
adsMediaSource.prepareSource(
|
||||||
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
|
||||||
refreshSourceInfo(timeline, manifest);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releaseSourceInternal() {
|
public void releaseSourceInternal() {
|
||||||
adsMediaSource.releaseSource(adsMediaSourceListener);
|
adsMediaSource.releaseSource(/* listener= */ this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSourceInfoRefreshed(
|
||||||
|
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||||
|
refreshSourceInfo(timeline, manifest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
extensions/ima/src/test/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<manifest package="com.google.android.exoplayer2.ext.ima.test" />
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.CompanionAd;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.UiElement;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** A fake ad for testing. */
|
||||||
|
/* package */ final class FakeAd implements Ad {
|
||||||
|
|
||||||
|
private final boolean skippable;
|
||||||
|
private final AdPodInfo adPodInfo;
|
||||||
|
|
||||||
|
public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) {
|
||||||
|
this.skippable = skippable;
|
||||||
|
adPodInfo =
|
||||||
|
new AdPodInfo() {
|
||||||
|
@Override
|
||||||
|
public int getTotalAds() {
|
||||||
|
return totalAds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAdPosition() {
|
||||||
|
return adPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPodIndex() {
|
||||||
|
return podIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBumper() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getMaxDuration() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getTimeOffset() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSkippable() {
|
||||||
|
return skippable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdPodInfo getAdPodInfo() {
|
||||||
|
return adPodInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdId() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreativeId() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreativeAdId() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUniversalAdIdValue() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUniversalAdIdRegistry() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdSystem() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getAdWrapperIds() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getAdWrapperSystems() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getAdWrapperCreativeIds() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLinear() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getSkipTimeOffset() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUiDisabled() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdvertiserName() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSurveyUrl() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDealId() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTraffickingParameters() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getDuration() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<UiElement> getUiElements() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CompanionAd> getCompanionAds() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsManager;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.StreamManager;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.StreamRequest;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */
|
||||||
|
public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader {
|
||||||
|
|
||||||
|
private final ImaSdkSettings imaSdkSettings;
|
||||||
|
private final AdsManager adsManager;
|
||||||
|
private final ArrayList<AdsLoadedListener> adsLoadedListeners;
|
||||||
|
private final ArrayList<AdErrorListener> adErrorListeners;
|
||||||
|
|
||||||
|
public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) {
|
||||||
|
this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);
|
||||||
|
this.adsManager = Assertions.checkNotNull(adsManager);
|
||||||
|
adsLoadedListeners = new ArrayList<>();
|
||||||
|
adErrorListeners = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contentComplete() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImaSdkSettings getSettings() {
|
||||||
|
return imaSdkSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestAds(AdsRequest adsRequest) {
|
||||||
|
for (AdsLoadedListener listener : adsLoadedListeners) {
|
||||||
|
listener.onAdsManagerLoaded(
|
||||||
|
new AdsManagerLoadedEvent() {
|
||||||
|
@Override
|
||||||
|
public AdsManager getAdsManager() {
|
||||||
|
return adsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamManager getStreamManager() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getUserRequestContext() {
|
||||||
|
return adsRequest.getUserRequestContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String requestStream(StreamRequest streamRequest) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
|
||||||
|
adsLoadedListeners.add(adsLoadedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
|
||||||
|
adsLoadedListeners.remove(adsLoadedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAdErrorListener(AdErrorListener adErrorListener) {
|
||||||
|
adErrorListeners.add(adErrorListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAdErrorListener(AdErrorListener adErrorListener) {
|
||||||
|
adErrorListeners.remove(adErrorListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Fake {@link AdsRequest} implementation for tests. */
|
||||||
|
public final class FakeAdsRequest implements AdsRequest {
|
||||||
|
|
||||||
|
private String adTagUrl;
|
||||||
|
private String adsResponse;
|
||||||
|
private Object userRequestContext;
|
||||||
|
private AdDisplayContainer adDisplayContainer;
|
||||||
|
private ContentProgressProvider contentProgressProvider;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdTagUrl(String adTagUrl) {
|
||||||
|
this.adTagUrl = adTagUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdTagUrl() {
|
||||||
|
return adTagUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setExtraParameter(String s, String s1) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtraParameter(String s) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getExtraParameters() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserRequestContext(Object userRequestContext) {
|
||||||
|
this.userRequestContext = userRequestContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getUserRequestContext() {
|
||||||
|
return userRequestContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdDisplayContainer getAdDisplayContainer() {
|
||||||
|
return adDisplayContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) {
|
||||||
|
this.adDisplayContainer = adDisplayContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContentProgressProvider getContentProgressProvider() {
|
||||||
|
return contentProgressProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) {
|
||||||
|
this.contentProgressProvider = contentProgressProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdsResponse() {
|
||||||
|
return adsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdsResponse(String adsResponse) {
|
||||||
|
this.adsResponse = adsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdWillAutoPlay(boolean b) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdWillPlayMuted(boolean b) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentDuration(float v) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentKeywords(List<String> list) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentTitle(String s) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setVastLoadTimeout(float v) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLiveStreamPrefetchSeconds(float v) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.testutil.StubExoPlayer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/** A fake player for testing content/ad playback. */
|
||||||
|
/* package */ final class FakePlayer extends StubExoPlayer {
|
||||||
|
|
||||||
|
private final ArrayList<Player.EventListener> listeners;
|
||||||
|
private final Timeline.Window window;
|
||||||
|
private final Timeline.Period period;
|
||||||
|
|
||||||
|
private boolean prepared;
|
||||||
|
private Timeline timeline;
|
||||||
|
private int state;
|
||||||
|
private boolean playWhenReady;
|
||||||
|
private long position;
|
||||||
|
private long contentPosition;
|
||||||
|
private boolean isPlayingAd;
|
||||||
|
private int adGroupIndex;
|
||||||
|
private int adIndexInAdGroup;
|
||||||
|
|
||||||
|
public FakePlayer() {
|
||||||
|
listeners = new ArrayList<>();
|
||||||
|
window = new Timeline.Window();
|
||||||
|
period = new Timeline.Period();
|
||||||
|
state = Player.STATE_IDLE;
|
||||||
|
playWhenReady = true;
|
||||||
|
timeline = Timeline.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
|
||||||
|
public void updateTimeline(Timeline timeline) {
|
||||||
|
for (Player.EventListener listener : listeners) {
|
||||||
|
listener.onTimelineChanged(
|
||||||
|
timeline,
|
||||||
|
null,
|
||||||
|
prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
|
||||||
|
}
|
||||||
|
prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state of this player as if it were playing content at the given {@code position}. If
|
||||||
|
* an ad is currently playing, this will trigger a position discontinuity.
|
||||||
|
*/
|
||||||
|
public void setPlayingContentPosition(long position) {
|
||||||
|
boolean notify = isPlayingAd;
|
||||||
|
isPlayingAd = false;
|
||||||
|
adGroupIndex = C.INDEX_UNSET;
|
||||||
|
adIndexInAdGroup = C.INDEX_UNSET;
|
||||||
|
this.position = position;
|
||||||
|
contentPosition = position;
|
||||||
|
if (notify) {
|
||||||
|
for (Player.EventListener listener : listeners) {
|
||||||
|
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state of this player as if it were playing an ad with the given indices at the given
|
||||||
|
* {@code position}. If the player is playing a different ad or content, this will trigger a
|
||||||
|
* position discontinuity.
|
||||||
|
*/
|
||||||
|
public void setPlayingAdPosition(
|
||||||
|
int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) {
|
||||||
|
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
|
||||||
|
isPlayingAd = true;
|
||||||
|
this.adGroupIndex = adGroupIndex;
|
||||||
|
this.adIndexInAdGroup = adIndexInAdGroup;
|
||||||
|
this.position = position;
|
||||||
|
this.contentPosition = contentPosition;
|
||||||
|
if (notify) {
|
||||||
|
for (Player.EventListener listener : listeners) {
|
||||||
|
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the state of this player with the given {@code STATE} constant. */
|
||||||
|
public void setState(int state, boolean playWhenReady) {
|
||||||
|
boolean notify = this.state != state || this.playWhenReady != playWhenReady;
|
||||||
|
this.state = state;
|
||||||
|
this.playWhenReady = playWhenReady;
|
||||||
|
if (notify) {
|
||||||
|
for (Player.EventListener listener : listeners) {
|
||||||
|
listener.onPlayerStateChanged(playWhenReady, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExoPlayer methods. Other methods are unsupported.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListener(Player.EventListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeListener(Player.EventListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPlaybackState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getPlayWhenReady() {
|
||||||
|
return playWhenReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Timeline getCurrentTimeline() {
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentPeriodIndex() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentWindowIndex() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNextWindowIndex() {
|
||||||
|
return C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPreviousWindowIndex() {
|
||||||
|
return C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDuration() {
|
||||||
|
if (timeline.isEmpty()) {
|
||||||
|
return C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
if (isPlayingAd()) {
|
||||||
|
long adDurationUs =
|
||||||
|
timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup);
|
||||||
|
return C.usToMs(adDurationUs);
|
||||||
|
} else {
|
||||||
|
return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCurrentPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPlayingAd() {
|
||||||
|
return isPlayingAd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentAdGroupIndex() {
|
||||||
|
return adGroupIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCurrentAdIndexInAdGroup() {
|
||||||
|
return adIndexInAdGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getContentPosition() {
|
||||||
|
return contentPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdEvent;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsManager;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||||
|
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
|
||||||
|
/** Test for {@link ImaAdsLoader}. */
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class ImaAdsLoaderTest {
|
||||||
|
|
||||||
|
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
|
||||||
|
private static final Timeline CONTENT_TIMELINE =
|
||||||
|
new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);
|
||||||
|
private static final Uri TEST_URI = Uri.EMPTY;
|
||||||
|
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
|
||||||
|
private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
|
||||||
|
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
|
||||||
|
private static final FakeAd UNSKIPPABLE_AD =
|
||||||
|
new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1);
|
||||||
|
|
||||||
|
private @Mock ImaSdkSettings imaSdkSettings;
|
||||||
|
private @Mock AdsRenderingSettings adsRenderingSettings;
|
||||||
|
private @Mock AdDisplayContainer adDisplayContainer;
|
||||||
|
private @Mock AdsManager adsManager;
|
||||||
|
private SingletonImaFactory testImaFactory;
|
||||||
|
private ViewGroup adUiViewGroup;
|
||||||
|
private TestAdsLoaderListener adsLoaderListener;
|
||||||
|
private FakePlayer fakeExoPlayer;
|
||||||
|
private ImaAdsLoader imaAdsLoader;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
FakeAdsRequest fakeAdsRequest = new FakeAdsRequest();
|
||||||
|
FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager);
|
||||||
|
testImaFactory =
|
||||||
|
new SingletonImaFactory(
|
||||||
|
imaSdkSettings,
|
||||||
|
adsRenderingSettings,
|
||||||
|
adDisplayContainer,
|
||||||
|
fakeAdsRequest,
|
||||||
|
fakeAdsLoader);
|
||||||
|
adUiViewGroup = new FrameLayout(RuntimeEnvironment.application);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void teardown() {
|
||||||
|
if (imaAdsLoader != null) {
|
||||||
|
imaAdsLoader.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuilder_overridesPlayerType() {
|
||||||
|
when(imaSdkSettings.getPlayerType()).thenReturn("test player type");
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
|
||||||
|
verify(imaSdkSettings).setPlayerType("google/exo.ext.ima");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttachPlayer_setsAdUiViewGroup() {
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
||||||
|
|
||||||
|
verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttachPlayer_updatesAdPlaybackState() {
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
||||||
|
|
||||||
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
|
.isEqualTo(
|
||||||
|
new AdPlaybackState(/* adGroupTimesUs= */ 0)
|
||||||
|
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttachAfterRelease() {
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
imaAdsLoader.release();
|
||||||
|
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttachAndCallbacksAfterRelease() {
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
imaAdsLoader.release();
|
||||||
|
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
||||||
|
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
||||||
|
fakeExoPlayer.setState(Player.STATE_READY, true);
|
||||||
|
|
||||||
|
// If callbacks are invoked there is no crash.
|
||||||
|
// Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown
|
||||||
|
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
|
||||||
|
// SDK being proguarded.
|
||||||
|
imaAdsLoader.requestAds(adUiViewGroup);
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.loadAd(TEST_URI.toString());
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.playAd();
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.pauseAd();
|
||||||
|
imaAdsLoader.stopAd();
|
||||||
|
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
|
||||||
|
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||||
|
imaAdsLoader.handlePrepareError(
|
||||||
|
/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlayback_withPrerollAd_marksAdAsPlayed() {
|
||||||
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
|
||||||
|
// Load the preroll ad.
|
||||||
|
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.loadAd(TEST_URI.toString());
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
||||||
|
|
||||||
|
// Play the preroll ad.
|
||||||
|
imaAdsLoader.playAd();
|
||||||
|
fakeExoPlayer.setPlayingAdPosition(
|
||||||
|
/* adGroupIndex= */ 0,
|
||||||
|
/* adIndexInAdGroup= */ 0,
|
||||||
|
/* position= */ 0,
|
||||||
|
/* contentPosition= */ 0);
|
||||||
|
fakeExoPlayer.setState(Player.STATE_READY, true);
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD));
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD));
|
||||||
|
|
||||||
|
// Play the content.
|
||||||
|
fakeExoPlayer.setPlayingContentPosition(0);
|
||||||
|
imaAdsLoader.stopAd();
|
||||||
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||||
|
|
||||||
|
// Verify that the preroll ad has been marked as played.
|
||||||
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
|
.isEqualTo(
|
||||||
|
new AdPlaybackState(/* adGroupTimesUs= */ 0)
|
||||||
|
.withContentDurationUs(CONTENT_DURATION_US)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||||
|
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
|
||||||
|
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
|
||||||
|
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||||
|
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
|
||||||
|
fakeExoPlayer = new FakePlayer();
|
||||||
|
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
||||||
|
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
|
||||||
|
imaAdsLoader =
|
||||||
|
new ImaAdsLoader.Builder(RuntimeEnvironment.application)
|
||||||
|
.setImaFactory(testImaFactory)
|
||||||
|
.setImaSdkSettings(imaSdkSettings)
|
||||||
|
.buildForAdTag(TEST_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
|
||||||
|
return new AdEvent() {
|
||||||
|
@Override
|
||||||
|
public AdEventType getType() {
|
||||||
|
return adEventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Ad getAd() {
|
||||||
|
return ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getAdData() {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ad loader event listener that forwards ad playback state to a fake player. */
|
||||||
|
private static final class TestAdsLoaderListener implements AdsLoader.EventListener {
|
||||||
|
|
||||||
|
private final FakePlayer fakeExoPlayer;
|
||||||
|
private final Timeline contentTimeline;
|
||||||
|
private final long[][] adDurationsUs;
|
||||||
|
|
||||||
|
public AdPlaybackState adPlaybackState;
|
||||||
|
|
||||||
|
public TestAdsLoaderListener(
|
||||||
|
FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {
|
||||||
|
this.fakeExoPlayer = fakeExoPlayer;
|
||||||
|
this.contentTimeline = contentTimeline;
|
||||||
|
this.adDurationsUs = adDurationsUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
||||||
|
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
|
||||||
|
this.adPlaybackState = adPlaybackState;
|
||||||
|
fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdLoadError(AdLoadException error, DataSpec dataSpec) {
|
||||||
|
assertThat(error.type).isNotEqualTo(AdLoadException.TYPE_UNEXPECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdClicked() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdTapped() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||||
|
|
||||||
|
/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */
|
||||||
|
final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
|
||||||
|
|
||||||
|
private final ImaSdkSettings imaSdkSettings;
|
||||||
|
private final AdsRenderingSettings adsRenderingSettings;
|
||||||
|
private final AdDisplayContainer adDisplayContainer;
|
||||||
|
private final AdsRequest adsRequest;
|
||||||
|
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
||||||
|
|
||||||
|
public SingletonImaFactory(
|
||||||
|
ImaSdkSettings imaSdkSettings,
|
||||||
|
AdsRenderingSettings adsRenderingSettings,
|
||||||
|
AdDisplayContainer adDisplayContainer,
|
||||||
|
AdsRequest adsRequest,
|
||||||
|
com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
|
||||||
|
this.imaSdkSettings = imaSdkSettings;
|
||||||
|
this.adsRenderingSettings = adsRenderingSettings;
|
||||||
|
this.adDisplayContainer = adDisplayContainer;
|
||||||
|
this.adsRequest = adsRequest;
|
||||||
|
this.adsLoader = adsLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImaSdkSettings createImaSdkSettings() {
|
||||||
|
return imaSdkSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdsRenderingSettings createAdsRenderingSettings() {
|
||||||
|
return adsRenderingSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdDisplayContainer createAdDisplayContainer() {
|
||||||
|
return adDisplayContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdsRequest createAdsRequest() {
|
||||||
|
return adsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||||
|
Context context, ImaSdkSettings imaSdkSettings) {
|
||||||
|
return adsLoader;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
extensions/ima/src/test/resources/robolectric.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -20,6 +20,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import com.firebase.jobdispatcher.JobService;
|
||||||
import com.firebase.jobdispatcher.Lifetime;
|
import com.firebase.jobdispatcher.Lifetime;
|
||||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -146,11 +147,14 @@ public final class JobDispatcherScheduler implements Scheduler {
|
||||||
public boolean onStartJob(JobParameters params) {
|
public boolean onStartJob(JobParameters params) {
|
||||||
logd("JobDispatcherSchedulerService is started");
|
logd("JobDispatcherSchedulerService is started");
|
||||||
Bundle extras = params.getExtras();
|
Bundle extras = params.getExtras();
|
||||||
|
Assertions.checkNotNull(extras, "Service started without extras.");
|
||||||
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
|
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
|
||||||
if (requirements.checkRequirements(this)) {
|
if (requirements.checkRequirements(this)) {
|
||||||
logd("Requirements are met");
|
logd("Requirements are met");
|
||||||
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
|
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
|
||||||
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
|
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
|
||||||
|
Assertions.checkNotNull(serviceAction, "Service action missing.");
|
||||||
|
Assertions.checkNotNull(servicePackage, "Service package missing.");
|
||||||
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
||||||
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
|
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
|
||||||
Util.startForegroundService(this, intent);
|
Util.startForegroundService(this, intent);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 17
|
minSdkVersion 17
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||||
import com.google.android.exoplayer2.video.VideoListener;
|
import com.google.android.exoplayer2.video.VideoListener;
|
||||||
|
|
||||||
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
|
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
|
||||||
public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
|
||||||
|
|
@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
private final Player player;
|
private final Player player;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
private final ComponentListener componentListener;
|
private final ComponentListener componentListener;
|
||||||
private final Runnable updateProgressRunnable;
|
private final int updatePeriodMs;
|
||||||
|
|
||||||
private @Nullable PlaybackPreparer playbackPreparer;
|
private @Nullable PlaybackPreparer playbackPreparer;
|
||||||
private ControlDispatcher controlDispatcher;
|
private ControlDispatcher controlDispatcher;
|
||||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||||
private boolean hasSurface;
|
private boolean hasSurface;
|
||||||
private boolean lastNotifiedPreparedState;
|
private boolean lastNotifiedPreparedState;
|
||||||
|
|
||||||
|
|
@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
|
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
this.updatePeriodMs = updatePeriodMs;
|
||||||
handler = new Handler();
|
handler = new Handler();
|
||||||
componentListener = new ComponentListener();
|
componentListener = new ComponentListener();
|
||||||
controlDispatcher = new DefaultControlDispatcher();
|
controlDispatcher = new DefaultControlDispatcher();
|
||||||
updateProgressRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Callback callback = getCallback();
|
|
||||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
|
||||||
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
|
|
||||||
handler.postDelayed(this, updatePeriodMs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
videoComponent.removeVideoListener(componentListener);
|
videoComponent.removeVideoListener(componentListener);
|
||||||
}
|
}
|
||||||
if (surfaceHolderGlueHost != null) {
|
if (surfaceHolderGlueHost != null) {
|
||||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
removeSurfaceHolderCallback(surfaceHolderGlueHost);
|
||||||
surfaceHolderGlueHost = null;
|
surfaceHolderGlueHost = null;
|
||||||
}
|
}
|
||||||
hasSurface = false;
|
hasSurface = false;
|
||||||
|
|
@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setProgressUpdatingEnabled(boolean enabled) {
|
public void setProgressUpdatingEnabled(boolean enabled) {
|
||||||
handler.removeCallbacks(updateProgressRunnable);
|
handler.removeCallbacks(this);
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
handler.post(updateProgressRunnable);
|
handler.post(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
&& (surfaceHolderGlueHost == null || hasSurface);
|
&& (surfaceHolderGlueHost == null || hasSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runnable implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Callback callback = getCallback();
|
||||||
|
callback.onCurrentPositionChanged(this);
|
||||||
|
callback.onBufferedPositionChanged(this);
|
||||||
|
handler.postDelayed(this, updatePeriodMs);
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
/* package */ void setVideoSurface(Surface surface) {
|
/* package */ void setVideoSurface(@Nullable Surface surface) {
|
||||||
hasSurface = surface != null;
|
hasSurface = surface != null;
|
||||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||||
if (videoComponent != null) {
|
if (videoComponent != null) {
|
||||||
|
|
@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ComponentListener extends Player.DefaultEventListener
|
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||||
implements SurfaceHolder.Callback, VideoListener {
|
private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {
|
||||||
|
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ComponentListener
|
||||||
|
implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
|
||||||
|
|
||||||
// SurfaceHolder.Callback implementation.
|
// SurfaceHolder.Callback implementation.
|
||||||
|
|
||||||
|
|
@ -281,8 +288,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);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2017 The Android Open Source Project
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -127,7 +127,7 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop(Player player) {
|
public void onStop(Player player) {
|
||||||
player.stop();
|
player.stop(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.ResultReceiver;
|
import android.os.ResultReceiver;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -46,25 +46,26 @@ import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects a {@link MediaSessionCompat} to a {@link Player}.
|
* Connects a {@link MediaSessionCompat} to a {@link Player}.
|
||||||
* <p>
|
*
|
||||||
* The connector listens for actions sent by the media session's controller and implements these
|
* <p>The connector listens for actions sent by the media session's controller and implements these
|
||||||
* actions by calling appropriate player methods. The playback state of the media session is
|
* actions by calling appropriate player methods. The playback state of the media session is
|
||||||
* automatically synced with the player. The connector can also be optionally extended by providing
|
* automatically synced with the player. The connector can also be optionally extended by providing
|
||||||
* various collaborators:
|
* various collaborators:
|
||||||
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
|
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
|
||||||
* {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
|
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
|
||||||
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
|
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
|
||||||
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way.
|
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
|
||||||
* </li>
|
* way.
|
||||||
* <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
|
* <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
|
||||||
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is
|
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
|
||||||
* recommended for most use cases.</li>
|
* is recommended for most use cases.
|
||||||
* <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling
|
* <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
|
||||||
* {@link #setQueueEditor(QueueEditor)}.</li>
|
* #setQueueEditor(QueueEditor)}.
|
||||||
* <li>An {@link ErrorMessageProvider} for providing human readable error messages and
|
* <li>An {@link ErrorMessageProvider} for providing human readable error messages and
|
||||||
* corresponding error codes can be set by calling
|
* corresponding error codes can be set by calling {@link
|
||||||
* {@link #setErrorMessageProvider(ErrorMessageProvider)}.</li>
|
* #setErrorMessageProvider(ErrorMessageProvider)}.
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public final class MediaSessionConnector {
|
public final class MediaSessionConnector {
|
||||||
|
|
@ -74,35 +75,30 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default repeat toggle modes which is the bitmask of
|
* The default repeat toggle modes which is the bitmask of {@link
|
||||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and
|
* RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
|
||||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
|
|
||||||
*/
|
*/
|
||||||
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
|
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
|
||||||
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
|
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
|
||||||
public static final String EXTRAS_PITCH = "EXO_PITCH";
|
|
||||||
private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
|
||||||
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
|
|
||||||
private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
|
|
||||||
| MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
|
|
||||||
|
|
||||||
/**
|
public static final String EXTRAS_PITCH = "EXO_PITCH";
|
||||||
* Receiver of media commands sent by a media controller.
|
private static final int BASE_MEDIA_SESSION_FLAGS =
|
||||||
*/
|
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
||||||
|
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
|
||||||
|
private static final int EDITOR_MEDIA_SESSION_FLAGS =
|
||||||
|
BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
|
||||||
|
|
||||||
|
/** Receiver of media commands sent by a media controller. */
|
||||||
public interface CommandReceiver {
|
public interface CommandReceiver {
|
||||||
/**
|
/**
|
||||||
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
|
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
|
||||||
*/
|
*/
|
||||||
String[] getCommands();
|
String[] getCommands();
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
|
|
||||||
*/
|
|
||||||
void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
|
void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Interface to which playback preparation actions are delegated. */
|
||||||
* Interface to which playback preparation actions are delegated.
|
|
||||||
*/
|
|
||||||
public interface PlaybackPreparer extends CommandReceiver {
|
public interface PlaybackPreparer extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS =
|
long ACTIONS =
|
||||||
|
|
@ -127,96 +123,77 @@ public final class MediaSessionConnector {
|
||||||
* @return The bitmask of the supported media actions.
|
* @return The bitmask of the supported media actions.
|
||||||
*/
|
*/
|
||||||
long getSupportedPrepareActions();
|
long getSupportedPrepareActions();
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPrepare()}.
|
|
||||||
*/
|
|
||||||
void onPrepare();
|
void onPrepare();
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
|
|
||||||
*/
|
|
||||||
void onPrepareFromMediaId(String mediaId, Bundle extras);
|
void onPrepareFromMediaId(String mediaId, Bundle extras);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
|
|
||||||
*/
|
|
||||||
void onPrepareFromSearch(String query, Bundle extras);
|
void onPrepareFromSearch(String query, Bundle extras);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
|
|
||||||
*/
|
|
||||||
void onPrepareFromUri(Uri uri, Bundle extras);
|
void onPrepareFromUri(Uri uri, Bundle extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Interface to which playback actions are delegated. */
|
||||||
* Interface to which playback actions are delegated.
|
|
||||||
*/
|
|
||||||
public interface PlaybackController extends CommandReceiver {
|
public interface PlaybackController extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
|
long ACTIONS =
|
||||||
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
|
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||||
| PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
|
| PlaybackStateCompat.ACTION_PLAY
|
||||||
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
| PlaybackStateCompat.ACTION_PAUSE
|
||||||
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
|
| PlaybackStateCompat.ACTION_SEEK_TO
|
||||||
|
| PlaybackStateCompat.ACTION_FAST_FORWARD
|
||||||
|
| PlaybackStateCompat.ACTION_REWIND
|
||||||
|
| PlaybackStateCompat.ACTION_STOP
|
||||||
|
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
||||||
|
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the actions which are supported by the controller. The supported actions must be a
|
* Returns the actions which are supported by the controller. The supported actions must be a
|
||||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
|
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
|
* PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
|
* PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP},
|
* PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and
|
* PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
|
* PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
|
||||||
*
|
*
|
||||||
* @param player The player.
|
* @param player The player.
|
||||||
* @return The bitmask of the supported media actions.
|
* @return The bitmask of the supported media actions.
|
||||||
*/
|
*/
|
||||||
long getSupportedPlaybackActions(@Nullable Player player);
|
long getSupportedPlaybackActions(@Nullable Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPlay()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPlay()}.
|
|
||||||
*/
|
|
||||||
void onPlay(Player player);
|
void onPlay(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onPause()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onPause()}.
|
|
||||||
*/
|
|
||||||
void onPause(Player player);
|
void onPause(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSeekTo(long)}.
|
|
||||||
*/
|
|
||||||
void onSeekTo(Player player, long position);
|
void onSeekTo(Player player, long position);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onFastForward()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onFastForward()}.
|
|
||||||
*/
|
|
||||||
void onFastForward(Player player);
|
void onFastForward(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onRewind()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onRewind()}.
|
|
||||||
*/
|
|
||||||
void onRewind(Player player);
|
void onRewind(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onStop()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onStop()}.
|
|
||||||
*/
|
|
||||||
void onStop(Player player);
|
void onStop(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}.
|
|
||||||
*/
|
|
||||||
void onSetShuffleMode(Player player, int shuffleMode);
|
void onSetShuffleMode(Player player, int shuffleMode);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}.
|
|
||||||
*/
|
|
||||||
void onSetRepeatMode(Player player, int repeatMode);
|
void onSetRepeatMode(Player player, int repeatMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles queue navigation actions, and updates the media session queue by calling
|
* Handles queue navigation actions, and updates the media session queue by calling {@code
|
||||||
* {@code MediaSessionCompat.setQueue()}.
|
* MediaSessionCompat.setQueue()}.
|
||||||
*/
|
*/
|
||||||
public interface QueueNavigator extends CommandReceiver {
|
public interface QueueNavigator extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
long ACTIONS =
|
||||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||||
|
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||||
|
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the actions which are supported by the navigator. The supported actions must be a
|
* Returns the actions which are supported by the navigator. The supported actions must be a
|
||||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
|
* bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
|
* PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
|
||||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
|
* PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @return The bitmask of the supported media actions.
|
* @return The bitmask of the supported media actions.
|
||||||
|
|
@ -235,34 +212,26 @@ public final class MediaSessionConnector {
|
||||||
*/
|
*/
|
||||||
void onCurrentWindowIndexChanged(Player player);
|
void onCurrentWindowIndexChanged(Player player);
|
||||||
/**
|
/**
|
||||||
* Gets the id of the currently active queue item, or
|
* Gets the id of the currently active queue item, or {@link
|
||||||
* {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
|
* MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
|
||||||
* <p>
|
*
|
||||||
* To let the connector publish metadata for the active queue item, the queue item with the
|
* <p>To let the connector publish metadata for the active queue item, the queue item with the
|
||||||
* returned id must be available in the list of items returned by
|
* returned id must be available in the list of items returned by {@link
|
||||||
* {@link MediaControllerCompat#getQueue()}.
|
* MediaControllerCompat#getQueue()}.
|
||||||
*
|
*
|
||||||
* @param player The player connected to the media session.
|
* @param player The player connected to the media session.
|
||||||
* @return The id of the active queue item.
|
* @return The id of the active queue item.
|
||||||
*/
|
*/
|
||||||
long getActiveQueueItemId(@Nullable Player player);
|
long getActiveQueueItemId(@Nullable Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
|
|
||||||
*/
|
|
||||||
void onSkipToPrevious(Player player);
|
void onSkipToPrevious(Player player);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
|
|
||||||
*/
|
|
||||||
void onSkipToQueueItem(Player player, long id);
|
void onSkipToQueueItem(Player player, long id);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onSkipToNext()}.
|
|
||||||
*/
|
|
||||||
void onSkipToNext(Player player);
|
void onSkipToNext(Player player);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Handles media session queue edits. */
|
||||||
* Handles media session queue edits.
|
|
||||||
*/
|
|
||||||
public interface QueueEditor extends CommandReceiver {
|
public interface QueueEditor extends CommandReceiver {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -270,8 +239,8 @@ public final class MediaSessionConnector {
|
||||||
*/
|
*/
|
||||||
void onAddQueueItem(Player player, MediaDescriptionCompat description);
|
void onAddQueueItem(Player player, MediaDescriptionCompat description);
|
||||||
/**
|
/**
|
||||||
* See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
|
* See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int
|
||||||
* int index)}.
|
* index)}.
|
||||||
*/
|
*/
|
||||||
void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
|
void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
|
||||||
/**
|
/**
|
||||||
|
|
@ -279,9 +248,7 @@ public final class MediaSessionConnector {
|
||||||
* description)}.
|
* description)}.
|
||||||
*/
|
*/
|
||||||
void onRemoveQueueItem(Player player, MediaDescriptionCompat description);
|
void onRemoveQueueItem(Player player, MediaDescriptionCompat description);
|
||||||
/**
|
/** See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */
|
||||||
* See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}.
|
|
||||||
*/
|
|
||||||
void onRemoveQueueItemAt(Player player, int index);
|
void onRemoveQueueItemAt(Player player, int index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,43 +275,49 @@ public final class MediaSessionConnector {
|
||||||
void onCustomAction(String action, Bundle extras);
|
void onCustomAction(String action, Bundle extras);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the
|
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
|
||||||
* media session by the connector or {@code null} if this action should not be published at the
|
* session by the connector or {@code null} if this action should not be published at the given
|
||||||
* given player state.
|
* player state.
|
||||||
*
|
*
|
||||||
* @return The custom action to be included in the session playback state or {@code null}.
|
* @return The custom action to be included in the session playback state or {@code null}.
|
||||||
*/
|
*/
|
||||||
PlaybackStateCompat.CustomAction getCustomAction();
|
PlaybackStateCompat.CustomAction getCustomAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Provides a {@link MediaMetadataCompat} for a given player state. */
|
||||||
* The wrapped {@link MediaSessionCompat}.
|
public interface MediaMetadataProvider {
|
||||||
*/
|
/**
|
||||||
|
* Gets the {@link MediaMetadataCompat} to be published to the session.
|
||||||
|
*
|
||||||
|
* @param player The player for which to provide metadata.
|
||||||
|
* @return The {@link MediaMetadataCompat} to be published to the session.
|
||||||
|
*/
|
||||||
|
MediaMetadataCompat getMetadata(Player player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The wrapped {@link MediaSessionCompat}. */
|
||||||
public final MediaSessionCompat mediaSession;
|
public final MediaSessionCompat mediaSession;
|
||||||
|
|
||||||
private final MediaControllerCompat mediaController;
|
private @Nullable final MediaMetadataProvider mediaMetadataProvider;
|
||||||
private final Handler handler;
|
|
||||||
private final boolean doMaintainMetadata;
|
|
||||||
private final ExoPlayerEventListener exoPlayerEventListener;
|
private final ExoPlayerEventListener exoPlayerEventListener;
|
||||||
private final MediaSessionCallback mediaSessionCallback;
|
private final MediaSessionCallback mediaSessionCallback;
|
||||||
private final PlaybackController playbackController;
|
private final PlaybackController playbackController;
|
||||||
private final String metadataExtrasPrefix;
|
|
||||||
private final Map<String, CommandReceiver> commandMap;
|
private final Map<String, CommandReceiver> commandMap;
|
||||||
|
|
||||||
private Player player;
|
private Player player;
|
||||||
private CustomActionProvider[] customActionProviders;
|
private CustomActionProvider[] customActionProviders;
|
||||||
private Map<String, CustomActionProvider> customActionMap;
|
private Map<String, CustomActionProvider> customActionMap;
|
||||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||||
|
private @Nullable Pair<Integer, CharSequence> customError;
|
||||||
private PlaybackPreparer playbackPreparer;
|
private PlaybackPreparer playbackPreparer;
|
||||||
private QueueNavigator queueNavigator;
|
private QueueNavigator queueNavigator;
|
||||||
private QueueEditor queueEditor;
|
private QueueEditor queueEditor;
|
||||||
private RatingCallback ratingCallback;
|
private RatingCallback ratingCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance. Must be called on the same thread that is used to construct the player
|
* Creates an instance.
|
||||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
*
|
||||||
* <p>
|
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
|
||||||
* Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
|
|
||||||
*
|
*
|
||||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||||
*/
|
*/
|
||||||
|
|
@ -353,17 +326,46 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance. Must be called on the same thread that is used to construct the player
|
* Creates an instance.
|
||||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
|
||||||
*
|
*
|
||||||
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}.
|
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, new
|
||||||
|
* DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
|
||||||
*
|
*
|
||||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||||
* @param playbackController A {@link PlaybackController} for handling playback actions.
|
* @param playbackController A {@link PlaybackController} for handling playback actions.
|
||||||
*/
|
*/
|
||||||
public MediaSessionConnector(
|
public MediaSessionConnector(
|
||||||
MediaSessionCompat mediaSession, PlaybackController playbackController) {
|
MediaSessionCompat mediaSession, PlaybackController playbackController) {
|
||||||
this(mediaSession, playbackController, true, null);
|
this(
|
||||||
|
mediaSession,
|
||||||
|
playbackController,
|
||||||
|
new DefaultMediaMetadataProvider(mediaSession.getController(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||||
|
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
|
||||||
|
* null} if the connector should handle playback actions directly.
|
||||||
|
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session.
|
||||||
|
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
|
||||||
|
* queue item to the session metadata.
|
||||||
|
* @deprecated Use {@link MediaSessionConnector#MediaSessionConnector(MediaSessionCompat,
|
||||||
|
* PlaybackController, MediaMetadataProvider)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public MediaSessionConnector(
|
||||||
|
MediaSessionCompat mediaSession,
|
||||||
|
@Nullable PlaybackController playbackController,
|
||||||
|
boolean doMaintainMetadata,
|
||||||
|
@Nullable String metadataExtrasPrefix) {
|
||||||
|
this(
|
||||||
|
mediaSession,
|
||||||
|
playbackController,
|
||||||
|
doMaintainMetadata
|
||||||
|
? new DefaultMediaMetadataProvider(mediaSession.getController(), metadataExtrasPrefix)
|
||||||
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -373,26 +375,19 @@ public final class MediaSessionConnector {
|
||||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||||
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
|
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
|
||||||
* null} if the connector should handle playback actions directly.
|
* null} if the connector should handle playback actions directly.
|
||||||
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
|
* @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
|
||||||
* {@code false}, you need to maintain the metadata of the media session yourself (provide at
|
* object to be published to the media session, or {@code null} if metadata shouldn't be
|
||||||
* least the duration to allow clients to show a progress bar).
|
* published.
|
||||||
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
|
|
||||||
* queue item to the session metadata.
|
|
||||||
*/
|
*/
|
||||||
public MediaSessionConnector(
|
public MediaSessionConnector(
|
||||||
MediaSessionCompat mediaSession,
|
MediaSessionCompat mediaSession,
|
||||||
PlaybackController playbackController,
|
@Nullable PlaybackController playbackController,
|
||||||
boolean doMaintainMetadata,
|
@Nullable MediaMetadataProvider mediaMetadataProvider) {
|
||||||
@Nullable String metadataExtrasPrefix) {
|
|
||||||
this.mediaSession = mediaSession;
|
this.mediaSession = mediaSession;
|
||||||
this.playbackController = playbackController != null ? playbackController
|
this.playbackController =
|
||||||
: new DefaultPlaybackController();
|
playbackController != null ? playbackController : new DefaultPlaybackController();
|
||||||
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
|
this.mediaMetadataProvider = mediaMetadataProvider;
|
||||||
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
|
|
||||||
: Looper.getMainLooper());
|
|
||||||
this.doMaintainMetadata = doMaintainMetadata;
|
|
||||||
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
||||||
mediaController = mediaSession.getController();
|
|
||||||
mediaSessionCallback = new MediaSessionCallback();
|
mediaSessionCallback = new MediaSessionCallback();
|
||||||
exoPlayerEventListener = new ExoPlayerEventListener();
|
exoPlayerEventListener = new ExoPlayerEventListener();
|
||||||
customActionMap = Collections.emptyMap();
|
customActionMap = Collections.emptyMap();
|
||||||
|
|
@ -401,7 +396,8 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the player to be connected to the media session.
|
* Sets the player to be connected to the media session. Must be called on the same thread that is
|
||||||
|
* used to access the player.
|
||||||
*
|
*
|
||||||
* <p>The order in which any {@link CustomActionProvider}s are passed determines the order of the
|
* <p>The order in which any {@link CustomActionProvider}s are passed determines the order of the
|
||||||
* actions published with the playback state of the session.
|
* actions published with the playback state of the session.
|
||||||
|
|
@ -425,14 +421,17 @@ public final class MediaSessionConnector {
|
||||||
this.playbackPreparer = playbackPreparer;
|
this.playbackPreparer = playbackPreparer;
|
||||||
registerCommandReceiver(playbackPreparer);
|
registerCommandReceiver(playbackPreparer);
|
||||||
|
|
||||||
this.customActionProviders = (player != null && customActionProviders != null)
|
this.customActionProviders =
|
||||||
? customActionProviders : new CustomActionProvider[0];
|
(player != null && customActionProviders != null)
|
||||||
|
? customActionProviders
|
||||||
|
: new CustomActionProvider[0];
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
Handler handler = new Handler(Util.getLooper());
|
||||||
mediaSession.setCallback(mediaSessionCallback, handler);
|
mediaSession.setCallback(mediaSessionCallback, handler);
|
||||||
player.addListener(exoPlayerEventListener);
|
player.addListener(exoPlayerEventListener);
|
||||||
}
|
}
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
updateMediaSessionMetadata();
|
invalidateMediaSessionMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -444,7 +443,7 @@ public final class MediaSessionConnector {
|
||||||
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||||
if (this.errorMessageProvider != errorMessageProvider) {
|
if (this.errorMessageProvider != errorMessageProvider) {
|
||||||
this.errorMessageProvider = errorMessageProvider;
|
this.errorMessageProvider = errorMessageProvider;
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,23 +489,50 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
/**
|
||||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
* Sets a custom error on the session.
|
||||||
for (String command : commandReceiver.getCommands()) {
|
*
|
||||||
commandMap.put(command, commandReceiver);
|
* <p>This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int,
|
||||||
}
|
* CharSequence)}. By default, the error code will be set to {@link
|
||||||
|
* PlaybackStateCompat#ERROR_CODE_APP_ERROR}.
|
||||||
|
*
|
||||||
|
* @param message The error string to report or {@code null} to clear the error.
|
||||||
|
*/
|
||||||
|
public void setCustomErrorMessage(@Nullable CharSequence message) {
|
||||||
|
int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR;
|
||||||
|
setCustomErrorMessage(message, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom error on the session.
|
||||||
|
*
|
||||||
|
* @param message The error string to report or {@code null} to clear the error.
|
||||||
|
* @param code The error code to report. Ignored when {@code message} is {@code null}.
|
||||||
|
*/
|
||||||
|
public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
|
||||||
|
customError = (message == null) ? null : new Pair<>(code, message);
|
||||||
|
invalidateMediaSessionPlaybackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the metadata of the media session.
|
||||||
|
*
|
||||||
|
* <p>Apps normally only need to call this method when the backing data for a given media item has
|
||||||
|
* changed and the metadata should be updated immediately.
|
||||||
|
*/
|
||||||
|
public final void invalidateMediaSessionMetadata() {
|
||||||
|
if (mediaMetadataProvider != null && player != null) {
|
||||||
|
mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
|
/**
|
||||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
* Updates the playback state of the media session.
|
||||||
for (String command : commandReceiver.getCommands()) {
|
*
|
||||||
commandMap.remove(command);
|
* <p>Apps normally only need to call this method when the custom actions provided by a {@link
|
||||||
}
|
* CustomActionProvider} changed and the playback state needs to be updated immediately.
|
||||||
}
|
*/
|
||||||
}
|
public final void invalidateMediaSessionPlaybackState() {
|
||||||
|
|
||||||
private void updateMediaSessionPlaybackState() {
|
|
||||||
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
|
builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
|
||||||
|
|
@ -527,36 +553,74 @@ public final class MediaSessionConnector {
|
||||||
int playbackState = player.getPlaybackState();
|
int playbackState = player.getPlaybackState();
|
||||||
ExoPlaybackException playbackError =
|
ExoPlaybackException playbackError =
|
||||||
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
||||||
|
boolean reportError = playbackError != null || customError != null;
|
||||||
int sessionPlaybackState =
|
int sessionPlaybackState =
|
||||||
playbackError != null
|
reportError
|
||||||
? PlaybackStateCompat.STATE_ERROR
|
? PlaybackStateCompat.STATE_ERROR
|
||||||
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
|
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
|
||||||
if (playbackError != null && errorMessageProvider != null) {
|
if (customError != null) {
|
||||||
|
builder.setErrorMessage(customError.first, customError.second);
|
||||||
|
} else if (playbackError != null && errorMessageProvider != null) {
|
||||||
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
|
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
|
||||||
builder.setErrorMessage(message.first, message.second);
|
builder.setErrorMessage(message.first, message.second);
|
||||||
}
|
}
|
||||||
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
|
long activeQueueItemId =
|
||||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
queueNavigator != null
|
||||||
|
? queueNavigator.getActiveQueueItemId(player)
|
||||||
|
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
|
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
|
||||||
builder.setActions(buildPlaybackActions())
|
builder
|
||||||
|
.setActions(buildPlaybackActions())
|
||||||
.setActiveQueueItemId(activeQueueItemId)
|
.setActiveQueueItemId(activeQueueItemId)
|
||||||
.setBufferedPosition(player.getBufferedPosition())
|
.setBufferedPosition(player.getBufferedPosition())
|
||||||
.setState(sessionPlaybackState, player.getCurrentPosition(),
|
.setState(
|
||||||
player.getPlaybackParameters().speed, SystemClock.elapsedRealtime())
|
sessionPlaybackState,
|
||||||
|
player.getCurrentPosition(),
|
||||||
|
player.getPlaybackParameters().speed,
|
||||||
|
SystemClock.elapsedRealtime())
|
||||||
.setExtras(extras);
|
.setExtras(extras);
|
||||||
mediaSession.setPlaybackState(builder.build());
|
mediaSession.setPlaybackState(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the queue of the media session by calling {@link
|
||||||
|
* QueueNavigator#onTimelineChanged(Player)}.
|
||||||
|
*
|
||||||
|
* <p>Apps normally only need to call this method when the backing data for a given queue item has
|
||||||
|
* changed and the queue should be updated immediately.
|
||||||
|
*/
|
||||||
|
public final void invalidateMediaSessionQueue() {
|
||||||
|
if (queueNavigator != null && player != null) {
|
||||||
|
queueNavigator.onTimelineChanged(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
||||||
|
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||||
|
for (String command : commandReceiver.getCommands()) {
|
||||||
|
commandMap.put(command, commandReceiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
|
||||||
|
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||||
|
for (String command : commandReceiver.getCommands()) {
|
||||||
|
commandMap.remove(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long buildPlaybackActions() {
|
private long buildPlaybackActions() {
|
||||||
long actions = (PlaybackController.ACTIONS
|
long actions =
|
||||||
& playbackController.getSupportedPlaybackActions(player));
|
(PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
||||||
}
|
}
|
||||||
if (queueNavigator != null) {
|
if (queueNavigator != null) {
|
||||||
actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
|
actions |=
|
||||||
player));
|
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
|
||||||
}
|
}
|
||||||
if (ratingCallback != null) {
|
if (ratingCallback != null) {
|
||||||
actions |= RatingCallback.ACTIONS;
|
actions |= RatingCallback.ACTIONS;
|
||||||
|
|
@ -564,17 +628,79 @@ public final class MediaSessionConnector {
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMediaSessionMetadata() {
|
private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
|
||||||
if (doMaintainMetadata) {
|
switch (exoPlayerPlaybackState) {
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
return PlaybackStateCompat.STATE_BUFFERING;
|
||||||
|
case Player.STATE_READY:
|
||||||
|
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
return PlaybackStateCompat.STATE_PAUSED;
|
||||||
|
default:
|
||||||
|
return PlaybackStateCompat.STATE_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDispatchToPlaybackPreparer(long action) {
|
||||||
|
return playbackPreparer != null
|
||||||
|
&& (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDispatchToRatingCallback(long action) {
|
||||||
|
return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDispatchToPlaybackController(long action) {
|
||||||
|
return (playbackController.getSupportedPlaybackActions(player)
|
||||||
|
& PlaybackController.ACTIONS
|
||||||
|
& action)
|
||||||
|
!= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDispatchToQueueNavigator(long action) {
|
||||||
|
return queueNavigator != null
|
||||||
|
&& (queueNavigator.getSupportedQueueNavigatorActions(player)
|
||||||
|
& QueueNavigator.ACTIONS
|
||||||
|
& action)
|
||||||
|
!= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default {@link MediaMetadataCompat} with properties and extras propagated from the
|
||||||
|
* active queue item to the session metadata.
|
||||||
|
*/
|
||||||
|
public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
|
||||||
|
|
||||||
|
private final MediaControllerCompat mediaController;
|
||||||
|
private final String metadataExtrasPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param mediaController The {@link MediaControllerCompat}.
|
||||||
|
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the
|
||||||
|
* active queue item to the session metadata.
|
||||||
|
*/
|
||||||
|
public DefaultMediaMetadataProvider(
|
||||||
|
MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) {
|
||||||
|
this.mediaController = mediaController;
|
||||||
|
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaMetadataCompat getMetadata(Player player) {
|
||||||
|
if (player.getCurrentTimeline().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
|
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
|
||||||
if (player != null && player.isPlayingAd()) {
|
if (player.isPlayingAd()) {
|
||||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
|
builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
|
||||||
}
|
}
|
||||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0
|
builder.putLong(
|
||||||
: player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
|
MediaMetadataCompat.METADATA_KEY_DURATION,
|
||||||
|
player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
|
||||||
if (queueNavigator != null) {
|
long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
|
||||||
long activeQueueItemId = queueNavigator.getActiveQueueItemId(player);
|
if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
|
||||||
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
|
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
|
||||||
for (int i = 0; queue != null && i < queue.size(); i++) {
|
for (int i = 0; queue != null && i < queue.size(); i++) {
|
||||||
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
||||||
|
|
@ -600,113 +726,92 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (description.getTitle() != null) {
|
if (description.getTitle() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
|
String title = String.valueOf(description.getTitle());
|
||||||
String.valueOf(description.getTitle()));
|
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
||||||
|
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
|
||||||
}
|
}
|
||||||
if (description.getSubtitle() != null) {
|
if (description.getSubtitle() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
builder.putString(
|
||||||
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
||||||
String.valueOf(description.getSubtitle()));
|
String.valueOf(description.getSubtitle()));
|
||||||
}
|
}
|
||||||
if (description.getDescription() != null) {
|
if (description.getDescription() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
builder.putString(
|
||||||
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||||
String.valueOf(description.getDescription()));
|
String.valueOf(description.getDescription()));
|
||||||
}
|
}
|
||||||
if (description.getIconBitmap() != null) {
|
if (description.getIconBitmap() != null) {
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
|
builder.putBitmap(
|
||||||
description.getIconBitmap());
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
|
||||||
}
|
}
|
||||||
if (description.getIconUri() != null) {
|
if (description.getIconUri() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
|
builder.putString(
|
||||||
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
|
||||||
String.valueOf(description.getIconUri()));
|
String.valueOf(description.getIconUri()));
|
||||||
}
|
}
|
||||||
if (description.getMediaId() != null) {
|
if (description.getMediaId() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
|
builder.putString(
|
||||||
|
MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
|
||||||
String.valueOf(description.getMediaId()));
|
String.valueOf(description.getMediaId()));
|
||||||
}
|
}
|
||||||
if (description.getMediaUri() != null) {
|
if (description.getMediaUri() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
|
builder.putString(
|
||||||
|
MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
|
||||||
String.valueOf(description.getMediaUri()));
|
String.valueOf(description.getMediaUri()));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaSession.setMetadata(builder.build());
|
return builder.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
|
private class ExoPlayerEventListener implements Player.EventListener {
|
||||||
switch (exoPlayerPlaybackState) {
|
|
||||||
case Player.STATE_BUFFERING:
|
|
||||||
return PlaybackStateCompat.STATE_BUFFERING;
|
|
||||||
case Player.STATE_READY:
|
|
||||||
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
|
|
||||||
case Player.STATE_ENDED:
|
|
||||||
return PlaybackStateCompat.STATE_PAUSED;
|
|
||||||
default:
|
|
||||||
return PlaybackStateCompat.STATE_NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canDispatchToPlaybackPreparer(long action) {
|
|
||||||
return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions()
|
|
||||||
& PlaybackPreparer.ACTIONS & action) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canDispatchToRatingCallback(long action) {
|
|
||||||
return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canDispatchToPlaybackController(long action) {
|
|
||||||
return (playbackController.getSupportedPlaybackActions(player)
|
|
||||||
& PlaybackController.ACTIONS & action) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canDispatchToQueueNavigator(long action) {
|
|
||||||
return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player)
|
|
||||||
& QueueNavigator.ACTIONS & action) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ExoPlayerEventListener extends Player.DefaultEventListener {
|
|
||||||
|
|
||||||
private int currentWindowIndex;
|
private int currentWindowIndex;
|
||||||
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) {
|
||||||
queueNavigator.onTimelineChanged(player);
|
queueNavigator.onTimelineChanged(player);
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
|
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
|
||||||
// active queue item and queue navigation actions may need to be updated
|
// active queue item and queue navigation actions may need to be updated
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
currentWindowCount = windowCount;
|
currentWindowCount = windowCount;
|
||||||
currentWindowIndex = windowIndex;
|
currentWindowIndex = windowIndex;
|
||||||
updateMediaSessionMetadata();
|
invalidateMediaSessionMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||||
mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE
|
mediaSession.setRepeatMode(
|
||||||
? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL
|
repeatMode == Player.REPEAT_MODE_ONE
|
||||||
? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE);
|
? PlaybackStateCompat.REPEAT_MODE_ONE
|
||||||
updateMediaSessionPlaybackState();
|
: repeatMode == Player.REPEAT_MODE_ALL
|
||||||
|
? PlaybackStateCompat.REPEAT_MODE_ALL
|
||||||
|
: PlaybackStateCompat.REPEAT_MODE_NONE);
|
||||||
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
|
mediaSession.setShuffleMode(
|
||||||
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
shuffleModeEnabled
|
||||||
updateMediaSessionPlaybackState();
|
? PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||||
|
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
||||||
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -716,16 +821,19 @@ public final class MediaSessionConnector {
|
||||||
queueNavigator.onCurrentWindowIndexChanged(player);
|
queueNavigator.onCurrentWindowIndexChanged(player);
|
||||||
}
|
}
|
||||||
currentWindowIndex = player.getCurrentWindowIndex();
|
currentWindowIndex = player.getCurrentWindowIndex();
|
||||||
updateMediaSessionMetadata();
|
// Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
|
||||||
|
// and before updating metadata.
|
||||||
|
invalidateMediaSessionPlaybackState();
|
||||||
|
invalidateMediaSessionMetadata();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MediaSessionCallback extends MediaSessionCompat.Callback {
|
private class MediaSessionCallback extends MediaSessionCompat.Callback {
|
||||||
|
|
@ -812,7 +920,7 @@ public final class MediaSessionConnector {
|
||||||
Map<String, CustomActionProvider> actionMap = customActionMap;
|
Map<String, CustomActionProvider> actionMap = customActionMap;
|
||||||
if (actionMap.containsKey(action)) {
|
if (actionMap.containsKey(action)) {
|
||||||
actionMap.get(action).onCustomAction(action, extras);
|
actionMap.get(action).onCustomAction(action, extras);
|
||||||
updateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -921,7 +1029,5 @@ public final class MediaSessionConnector {
|
||||||
queueEditor.onRemoveQueueItemAt(player, index);
|
queueEditor.onRemoveQueueItemAt(player, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2017 The Android Open Source Project
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2017 The Android Open Source Project
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
||||||
|
|
||||||
private void publishFloatingQueueWindow(Player player) {
|
private void publishFloatingQueueWindow(Player player) {
|
||||||
if (player.getCurrentTimeline().isEmpty()) {
|
if (player.getCurrentTimeline().isEmpty()) {
|
||||||
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
|
mediaSession.setQueue(Collections.emptyList());
|
||||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
@ -28,7 +33,8 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
api 'com.squareup.okhttp3:okhttp:3.10.0'
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
api 'com.squareup.okhttp3:okhttp:3.11.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,25 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.okhttp;
|
package com.google.android.exoplayer2.ext.okhttp;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
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.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
import com.google.android.exoplayer2.util.Predicate;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import okhttp3.CacheControl;
|
import okhttp3.CacheControl;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
|
|
@ -40,30 +41,28 @@ import okhttp3.MediaType;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
/**
|
/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */
|
||||||
* An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
|
public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
*/
|
|
||||||
public class OkHttpDataSource implements HttpDataSource {
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
|
private static final byte[] SKIP_BUFFER = new byte[4096];
|
||||||
|
|
||||||
@NonNull private final Call.Factory callFactory;
|
private final Call.Factory callFactory;
|
||||||
@NonNull private final RequestProperties requestProperties;
|
private final RequestProperties requestProperties;
|
||||||
|
|
||||||
@Nullable private final String userAgent;
|
private final @Nullable String userAgent;
|
||||||
@Nullable private final Predicate<String> contentTypePredicate;
|
private final @Nullable Predicate<String> contentTypePredicate;
|
||||||
@Nullable private final TransferListener<? super OkHttpDataSource> listener;
|
private final @Nullable CacheControl cacheControl;
|
||||||
@Nullable private final CacheControl cacheControl;
|
private final @Nullable RequestProperties defaultRequestProperties;
|
||||||
@Nullable private final RequestProperties defaultRequestProperties;
|
|
||||||
|
|
||||||
private DataSpec dataSpec;
|
private @Nullable DataSpec dataSpec;
|
||||||
private Response response;
|
private @Nullable Response response;
|
||||||
private InputStream responseByteStream;
|
private @Nullable InputStream responseByteStream;
|
||||||
private boolean opened;
|
private boolean opened;
|
||||||
|
|
||||||
private long bytesToSkip;
|
private long bytesToSkip;
|
||||||
|
|
@ -77,11 +76,19 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
* by the source.
|
* by the source.
|
||||||
* @param userAgent An optional User-Agent string.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* #open(DataSpec)}.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
public OkHttpDataSource(
|
||||||
|
Call.Factory callFactory,
|
||||||
|
@Nullable String userAgent,
|
||||||
@Nullable Predicate<String> contentTypePredicate) {
|
@Nullable Predicate<String> contentTypePredicate) {
|
||||||
this(callFactory, userAgent, contentTypePredicate, null);
|
this(
|
||||||
|
callFactory,
|
||||||
|
userAgent,
|
||||||
|
contentTypePredicate,
|
||||||
|
/* cacheControl= */ null,
|
||||||
|
/* defaultRequestProperties= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -89,49 +96,35 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
* by the source.
|
* by the source.
|
||||||
* @param userAgent An optional User-Agent string.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* {@link #open(DataSpec)}.
|
* #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
|
||||||
*/
|
|
||||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
|
||||||
@Nullable Predicate<String> contentTypePredicate,
|
|
||||||
@Nullable TransferListener<? super OkHttpDataSource> listener) {
|
|
||||||
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
|
||||||
* by the source.
|
|
||||||
* @param userAgent An optional User-Agent string.
|
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
|
||||||
* {@link #open(DataSpec)}.
|
|
||||||
* @param listener An optional listener.
|
|
||||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||||
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
||||||
* the server as HTTP headers on every request.
|
* the server as HTTP headers on every request.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
public OkHttpDataSource(
|
||||||
|
Call.Factory callFactory,
|
||||||
|
@Nullable String userAgent,
|
||||||
@Nullable Predicate<String> contentTypePredicate,
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
@Nullable TransferListener<? super OkHttpDataSource> listener,
|
@Nullable CacheControl cacheControl,
|
||||||
@Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
|
@Nullable RequestProperties defaultRequestProperties) {
|
||||||
|
super(/* isNetwork= */ true);
|
||||||
this.callFactory = Assertions.checkNotNull(callFactory);
|
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
|
||||||
this.cacheControl = cacheControl;
|
this.cacheControl = cacheControl;
|
||||||
this.defaultRequestProperties = defaultRequestProperties;
|
this.defaultRequestProperties = defaultRequestProperties;
|
||||||
this.requestProperties = new RequestProperties();
|
this.requestProperties = new RequestProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri getUri() {
|
public @Nullable Uri getUri() {
|
||||||
return response == null ? null : Uri.parse(response.request().url().toString());
|
return response == null ? null : Uri.parse(response.request().url().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, List<String>> getResponseHeaders() {
|
public Map<String, List<String>> getResponseHeaders() {
|
||||||
return response == null ? null : response.headers().toMultimap();
|
return response == null ? Collections.emptyMap() : response.headers().toMultimap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -157,10 +150,16 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
this.dataSpec = dataSpec;
|
this.dataSpec = dataSpec;
|
||||||
this.bytesRead = 0;
|
this.bytesRead = 0;
|
||||||
this.bytesSkipped = 0;
|
this.bytesSkipped = 0;
|
||||||
|
transferInitializing(dataSpec);
|
||||||
|
|
||||||
Request request = makeRequest(dataSpec);
|
Request request = makeRequest(dataSpec);
|
||||||
|
Response response;
|
||||||
|
ResponseBody responseBody;
|
||||||
try {
|
try {
|
||||||
response = callFactory.newCall(request).execute();
|
this.response = callFactory.newCall(request).execute();
|
||||||
responseByteStream = response.body().byteStream();
|
response = this.response;
|
||||||
|
responseBody = Assertions.checkNotNull(response.body());
|
||||||
|
responseByteStream = responseBody.byteStream();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
|
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
|
||||||
dataSpec, HttpDataSourceException.TYPE_OPEN);
|
dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||||
|
|
@ -181,8 +180,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a valid content type.
|
// Check for a valid content type.
|
||||||
MediaType mediaType = response.body().contentType();
|
MediaType mediaType = responseBody.contentType();
|
||||||
String contentType = mediaType != null ? mediaType.toString() : null;
|
String contentType = mediaType != null ? mediaType.toString() : "";
|
||||||
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
|
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
|
||||||
closeConnectionQuietly();
|
closeConnectionQuietly();
|
||||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||||
|
|
@ -197,14 +196,12 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
bytesToRead = dataSpec.length;
|
bytesToRead = dataSpec.length;
|
||||||
} else {
|
} else {
|
||||||
long contentLength = response.body().contentLength();
|
long contentLength = responseBody.contentLength();
|
||||||
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
opened = true;
|
opened = true;
|
||||||
if (listener != null) {
|
transferStarted(dataSpec);
|
||||||
listener.onTransferStart(this, dataSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytesToRead;
|
return bytesToRead;
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +212,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
skipInternal();
|
skipInternal();
|
||||||
return readInternal(buffer, offset, readLength);
|
return readInternal(buffer, offset, readLength);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
|
throw new HttpDataSourceException(
|
||||||
|
e, Assertions.checkNotNull(dataSpec), HttpDataSourceException.TYPE_READ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,9 +221,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
public void close() throws HttpDataSourceException {
|
public void close() throws HttpDataSourceException {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
opened = false;
|
opened = false;
|
||||||
if (listener != null) {
|
transferEnded();
|
||||||
listener.onTransferEnd(this);
|
|
||||||
}
|
|
||||||
closeConnectionQuietly();
|
closeConnectionQuietly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,15 +258,18 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
|
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Establishes a connection. */
|
||||||
* Establishes a connection.
|
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
|
||||||
*/
|
|
||||||
private Request makeRequest(DataSpec dataSpec) {
|
|
||||||
long position = dataSpec.position;
|
long position = dataSpec.position;
|
||||||
long length = dataSpec.length;
|
long length = dataSpec.length;
|
||||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
||||||
|
if (url == null) {
|
||||||
|
throw new HttpDataSourceException(
|
||||||
|
"Malformed URL", dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||||
|
}
|
||||||
|
|
||||||
Request.Builder builder = new Request.Builder().url(url);
|
Request.Builder builder = new Request.Builder().url(url);
|
||||||
if (cacheControl != null) {
|
if (cacheControl != null) {
|
||||||
builder.cacheControl(cacheControl);
|
builder.cacheControl(cacheControl);
|
||||||
|
|
@ -297,9 +296,14 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
if (!allowGzip) {
|
if (!allowGzip) {
|
||||||
builder.addHeader("Accept-Encoding", "identity");
|
builder.addHeader("Accept-Encoding", "identity");
|
||||||
}
|
}
|
||||||
if (dataSpec.postBody != null) {
|
RequestBody requestBody = null;
|
||||||
builder.post(RequestBody.create(null, dataSpec.postBody));
|
if (dataSpec.httpBody != null) {
|
||||||
|
requestBody = RequestBody.create(null, dataSpec.httpBody);
|
||||||
|
} else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||||
|
// OkHttp requires a non-null body for POST requests.
|
||||||
|
requestBody = RequestBody.create(null, new byte[0]);
|
||||||
}
|
}
|
||||||
|
builder.method(dataSpec.getHttpMethodString(), requestBody);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,15 +320,9 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire the shared skip buffer.
|
|
||||||
byte[] skipBuffer = skipBufferReference.getAndSet(null);
|
|
||||||
if (skipBuffer == null) {
|
|
||||||
skipBuffer = new byte[4096];
|
|
||||||
}
|
|
||||||
|
|
||||||
while (bytesSkipped != bytesToSkip) {
|
while (bytesSkipped != bytesToSkip) {
|
||||||
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
|
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
|
||||||
int read = responseByteStream.read(skipBuffer, 0, readLength);
|
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
|
||||||
if (Thread.currentThread().isInterrupted()) {
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
throw new InterruptedIOException();
|
throw new InterruptedIOException();
|
||||||
}
|
}
|
||||||
|
|
@ -332,13 +330,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
throw new EOFException();
|
throw new EOFException();
|
||||||
}
|
}
|
||||||
bytesSkipped += read;
|
bytesSkipped += read;
|
||||||
if (listener != null) {
|
bytesTransferred(read);
|
||||||
listener.onBytesTransferred(this, read);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release the shared skip buffer.
|
|
||||||
skipBufferReference.set(skipBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -367,7 +360,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
readLength = (int) Math.min(readLength, bytesRemaining);
|
readLength = (int) Math.min(readLength, bytesRemaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
int read = responseByteStream.read(buffer, offset, readLength);
|
int read = castNonNull(responseByteStream).read(buffer, offset, readLength);
|
||||||
if (read == -1) {
|
if (read == -1) {
|
||||||
if (bytesToRead != C.LENGTH_UNSET) {
|
if (bytesToRead != C.LENGTH_UNSET) {
|
||||||
// End of stream reached having not read sufficient data.
|
// End of stream reached having not read sufficient data.
|
||||||
|
|
@ -377,9 +370,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
bytesRead += read;
|
bytesRead += read;
|
||||||
if (listener != null) {
|
bytesTransferred(read);
|
||||||
listener.onBytesTransferred(this, read);
|
|
||||||
}
|
|
||||||
return read;
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,8 +378,10 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
* Closes the current connection quietly, if there is one.
|
* Closes the current connection quietly, if there is one.
|
||||||
*/
|
*/
|
||||||
private void closeConnectionQuietly() {
|
private void closeConnectionQuietly() {
|
||||||
response.body().close();
|
if (response != null) {
|
||||||
response = null;
|
Assertions.checkNotNull(response.body()).close();
|
||||||
|
response = null;
|
||||||
|
}
|
||||||
responseByteStream = null;
|
responseByteStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.okhttp;
|
package com.google.android.exoplayer2.ext.okhttp;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||||
|
|
@ -30,10 +28,30 @@ import okhttp3.Call;
|
||||||
*/
|
*/
|
||||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
@NonNull private final Call.Factory callFactory;
|
private final Call.Factory callFactory;
|
||||||
@Nullable private final String userAgent;
|
private final @Nullable String userAgent;
|
||||||
@Nullable private final TransferListener<? super DataSource> listener;
|
private final @Nullable TransferListener listener;
|
||||||
@Nullable private final CacheControl cacheControl;
|
private final @Nullable CacheControl cacheControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
|
* by the sources created by the factory.
|
||||||
|
* @param userAgent An optional User-Agent string.
|
||||||
|
*/
|
||||||
|
public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent) {
|
||||||
|
this(callFactory, userAgent, /* listener= */ null, /* cacheControl= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
|
* by the sources created by the factory.
|
||||||
|
* @param userAgent An optional User-Agent string.
|
||||||
|
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||||
|
*/
|
||||||
|
public OkHttpDataSourceFactory(
|
||||||
|
Call.Factory callFactory, @Nullable String userAgent, @Nullable CacheControl cacheControl) {
|
||||||
|
this(callFactory, userAgent, /* listener= */ null, cacheControl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
|
|
@ -41,9 +59,9 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||||
* @param userAgent An optional User-Agent string.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
public OkHttpDataSourceFactory(
|
||||||
@Nullable TransferListener<? super DataSource> listener) {
|
Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) {
|
||||||
this(callFactory, userAgent, listener, null);
|
this(callFactory, userAgent, listener, /* cacheControl= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,8 +71,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
public OkHttpDataSourceFactory(
|
||||||
@Nullable TransferListener<? super DataSource> listener,
|
Call.Factory callFactory,
|
||||||
|
@Nullable String userAgent,
|
||||||
|
@Nullable TransferListener listener,
|
||||||
@Nullable CacheControl cacheControl) {
|
@Nullable CacheControl cacheControl) {
|
||||||
this.callFactory = callFactory;
|
this.callFactory = callFactory;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
|
|
@ -65,8 +85,16 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||||
@Override
|
@Override
|
||||||
protected OkHttpDataSource createDataSourceInternal(
|
protected OkHttpDataSource createDataSourceInternal(
|
||||||
HttpDataSource.RequestProperties defaultRequestProperties) {
|
HttpDataSource.RequestProperties defaultRequestProperties) {
|
||||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
|
OkHttpDataSource dataSource =
|
||||||
defaultRequestProperties);
|
new OkHttpDataSource(
|
||||||
|
callFactory,
|
||||||
|
userAgent,
|
||||||
|
/* contentTypePredicate= */ null,
|
||||||
|
cacheControl,
|
||||||
|
defaultRequestProperties);
|
||||||
|
if (listener != null) {
|
||||||
|
dataSource.addTransferListener(listener);
|
||||||
|
}
|
||||||
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||||
implements Runnable {
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
*/
|
*/
|
||||||
public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
|
|
||||||
|
/** The number of input and output buffers. */
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
|
/** The default input buffer size. */
|
||||||
|
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
|
||||||
|
|
||||||
private OpusDecoder decoder;
|
private OpusDecoder decoder;
|
||||||
|
|
||||||
|
|
@ -88,8 +90,15 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
throws OpusDecoderException {
|
throws OpusDecoderException {
|
||||||
decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
int initialInputBufferSize =
|
||||||
format.initializationData, mediaCrypto);
|
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||||
|
decoder =
|
||||||
|
new OpusDecoder(
|
||||||
|
NUM_BUFFERS,
|
||||||
|
NUM_BUFFERS,
|
||||||
|
initialInputBufferSize,
|
||||||
|
format.initializationData,
|
||||||
|
mediaCrypto);
|
||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
||||||
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.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
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 com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
@ -26,40 +27,40 @@ import java.io.IOException;
|
||||||
import net.butterflytv.rtmp_client.RtmpClient;
|
import net.butterflytv.rtmp_client.RtmpClient;
|
||||||
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
|
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
|
||||||
|
|
||||||
/**
|
/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */
|
||||||
* A Real-Time Messaging Protocol (RTMP) {@link DataSource}.
|
public final class RtmpDataSource extends BaseDataSource {
|
||||||
*/
|
|
||||||
public final class RtmpDataSource implements DataSource {
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable private final TransferListener<? super RtmpDataSource> listener;
|
|
||||||
|
|
||||||
private RtmpClient rtmpClient;
|
private RtmpClient rtmpClient;
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
|
|
||||||
public RtmpDataSource() {
|
public RtmpDataSource() {
|
||||||
this(null);
|
super(/* isNetwork= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
|
* @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
|
||||||
*/
|
*/
|
||||||
public RtmpDataSource(@Nullable TransferListener<? super RtmpDataSource> listener) {
|
@Deprecated
|
||||||
this.listener = listener;
|
public RtmpDataSource(@Nullable TransferListener listener) {
|
||||||
|
this();
|
||||||
|
if (listener != null) {
|
||||||
|
addTransferListener(listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long open(DataSpec dataSpec) throws RtmpIOException {
|
public long open(DataSpec dataSpec) throws RtmpIOException {
|
||||||
|
transferInitializing(dataSpec);
|
||||||
rtmpClient = new RtmpClient();
|
rtmpClient = new RtmpClient();
|
||||||
rtmpClient.open(dataSpec.uri.toString(), false);
|
rtmpClient.open(dataSpec.uri.toString(), false);
|
||||||
|
|
||||||
this.uri = dataSpec.uri;
|
this.uri = dataSpec.uri;
|
||||||
if (listener != null) {
|
transferStarted(dataSpec);
|
||||||
listener.onTransferStart(this, dataSpec);
|
|
||||||
}
|
|
||||||
return C.LENGTH_UNSET;
|
return C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,9 +70,7 @@ public final class RtmpDataSource implements DataSource {
|
||||||
if (bytesRead == -1) {
|
if (bytesRead == -1) {
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
if (listener != null) {
|
bytesTransferred(bytesRead);
|
||||||
listener.onBytesTransferred(this, bytesRead);
|
|
||||||
}
|
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,9 +78,7 @@ public final class RtmpDataSource implements DataSource {
|
||||||
public void close() {
|
public void close() {
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
uri = null;
|
uri = null;
|
||||||
if (listener != null) {
|
transferEnded();
|
||||||
listener.onTransferEnd(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (rtmpClient != null) {
|
if (rtmpClient != null) {
|
||||||
rtmpClient.close();
|
rtmpClient.close();
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,14 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
*/
|
*/
|
||||||
public final class RtmpDataSourceFactory implements DataSource.Factory {
|
public final class RtmpDataSourceFactory implements DataSource.Factory {
|
||||||
|
|
||||||
@Nullable
|
private final @Nullable TransferListener listener;
|
||||||
private final TransferListener<? super RtmpDataSource> listener;
|
|
||||||
|
|
||||||
public RtmpDataSourceFactory() {
|
public RtmpDataSourceFactory() {
|
||||||
this(null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @param listener An optional listener. */
|
||||||
* @param listener An optional listener.
|
public RtmpDataSourceFactory(@Nullable TransferListener listener) {
|
||||||
*/
|
|
||||||
public RtmpDataSourceFactory(@Nullable TransferListener<? super RtmpDataSource> listener) {
|
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||||
implements Runnable {
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
* requiring multiple output buffers to be dequeued at a time for it to make progress.
|
* requiring multiple output buffers to be dequeued at a time for it to make progress.
|
||||||
*/
|
*/
|
||||||
private static final int NUM_OUTPUT_BUFFERS = 8;
|
private static final int NUM_OUTPUT_BUFFERS = 8;
|
||||||
/**
|
/** The default input buffer size. */
|
||||||
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
|
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
||||||
* insufficient.
|
|
||||||
*/
|
|
||||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
|
||||||
|
|
||||||
private final boolean scaleToFit;
|
private final boolean scaleToFit;
|
||||||
private final boolean disableLoopFilter;
|
private final boolean disableLoopFilter;
|
||||||
|
|
@ -114,6 +111,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
private final FormatHolder formatHolder;
|
private final FormatHolder formatHolder;
|
||||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||||
|
private final boolean useSurfaceYuvOutput;
|
||||||
|
|
||||||
private Format format;
|
private Format format;
|
||||||
private VpxDecoder decoder;
|
private VpxDecoder decoder;
|
||||||
|
|
@ -177,7 +175,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
maxDroppedFramesToNotify,
|
maxDroppedFramesToNotify,
|
||||||
/* drmSessionManager= */ null,
|
/* drmSessionManager= */ null,
|
||||||
/* playClearSamplesWithoutKeys= */ false,
|
/* playClearSamplesWithoutKeys= */ false,
|
||||||
/* disableLoopFilter= */ false);
|
/* disableLoopFilter= */ false,
|
||||||
|
/* useSurfaceYuvOutput= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -197,11 +196,18 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||||
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
||||||
|
* @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
|
||||||
*/
|
*/
|
||||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
public LibvpxVideoRenderer(
|
||||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
boolean scaleToFit,
|
||||||
int maxDroppedFramesToNotify, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
long allowedJoiningTimeMs,
|
||||||
boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) {
|
Handler eventHandler,
|
||||||
|
VideoRendererEventListener eventListener,
|
||||||
|
int maxDroppedFramesToNotify,
|
||||||
|
DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||||
|
boolean playClearSamplesWithoutKeys,
|
||||||
|
boolean disableLoopFilter,
|
||||||
|
boolean useSurfaceYuvOutput) {
|
||||||
super(C.TRACK_TYPE_VIDEO);
|
super(C.TRACK_TYPE_VIDEO);
|
||||||
this.scaleToFit = scaleToFit;
|
this.scaleToFit = scaleToFit;
|
||||||
this.disableLoopFilter = disableLoopFilter;
|
this.disableLoopFilter = disableLoopFilter;
|
||||||
|
|
@ -209,6 +215,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
|
this.useSurfaceYuvOutput = useSurfaceYuvOutput;
|
||||||
joiningDeadlineMs = C.TIME_UNSET;
|
joiningDeadlineMs = C.TIME_UNSET;
|
||||||
clearReportedVideoSize();
|
clearReportedVideoSize();
|
||||||
formatHolder = new FormatHolder();
|
formatHolder = new FormatHolder();
|
||||||
|
|
@ -549,21 +556,25 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
*
|
*
|
||||||
* @param outputBuffer The buffer to render.
|
* @param outputBuffer The buffer to render.
|
||||||
*/
|
*/
|
||||||
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) {
|
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
|
||||||
int bufferMode = outputBuffer.mode;
|
int bufferMode = outputBuffer.mode;
|
||||||
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
||||||
|
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
|
||||||
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
||||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||||
if (!renderRgb && !renderYuv) {
|
if (!renderRgb && !renderYuv && !renderSurface) {
|
||||||
dropOutputBuffer(outputBuffer);
|
dropOutputBuffer(outputBuffer);
|
||||||
} else {
|
} else {
|
||||||
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
||||||
if (renderRgb) {
|
if (renderRgb) {
|
||||||
renderRgbFrame(outputBuffer, scaleToFit);
|
renderRgbFrame(outputBuffer, scaleToFit);
|
||||||
outputBuffer.release();
|
outputBuffer.release();
|
||||||
} else /* renderYuv */ {
|
} else if (renderYuv) {
|
||||||
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
||||||
// The renderer will release the buffer.
|
// The renderer will release the buffer.
|
||||||
|
} else { // renderSurface
|
||||||
|
decoder.renderToSurface(outputBuffer, surface);
|
||||||
|
outputBuffer.release();
|
||||||
}
|
}
|
||||||
consecutiveDroppedFrameCount = 0;
|
consecutiveDroppedFrameCount = 0;
|
||||||
decoderCounters.renderedOutputBufferCount++;
|
decoderCounters.renderedOutputBufferCount++;
|
||||||
|
|
@ -633,8 +644,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
// The output has changed.
|
// The output has changed.
|
||||||
this.surface = surface;
|
this.surface = surface;
|
||||||
this.outputBufferRenderer = outputBufferRenderer;
|
this.outputBufferRenderer = outputBufferRenderer;
|
||||||
outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV
|
if (surface != null) {
|
||||||
: surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE;
|
outputMode =
|
||||||
|
useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
|
||||||
|
} else {
|
||||||
|
outputMode =
|
||||||
|
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
|
||||||
|
}
|
||||||
if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
|
if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.setOutputMode(outputMode);
|
decoder.setOutputMode(outputMode);
|
||||||
|
|
@ -684,13 +700,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
try {
|
try {
|
||||||
long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
|
long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("createVpxDecoder");
|
TraceUtil.beginSection("createVpxDecoder");
|
||||||
|
int initialInputBufferSize =
|
||||||
|
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||||
decoder =
|
decoder =
|
||||||
new VpxDecoder(
|
new VpxDecoder(
|
||||||
NUM_INPUT_BUFFERS,
|
NUM_INPUT_BUFFERS,
|
||||||
NUM_OUTPUT_BUFFERS,
|
NUM_OUTPUT_BUFFERS,
|
||||||
INITIAL_INPUT_BUFFER_SIZE,
|
initialInputBufferSize,
|
||||||
mediaCrypto,
|
mediaCrypto,
|
||||||
disableLoopFilter);
|
disableLoopFilter,
|
||||||
|
useSurfaceYuvOutput);
|
||||||
decoder.setOutputMode(outputMode);
|
decoder.setOutputMode(outputMode);
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
|
@ -817,7 +836,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
||||||
*/
|
*/
|
||||||
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException, VpxDecoderException {
|
||||||
if (initialPositionUs == C.TIME_UNSET) {
|
if (initialPositionUs == C.TIME_UNSET) {
|
||||||
initialPositionUs = positionUs;
|
initialPositionUs = positionUs;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.vp9;
|
package com.google.android.exoplayer2.ext.vp9;
|
||||||
|
|
||||||
|
import android.view.Surface;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
|
|
@ -31,6 +32,7 @@ import java.nio.ByteBuffer;
|
||||||
public static final int OUTPUT_MODE_NONE = -1;
|
public static final int OUTPUT_MODE_NONE = -1;
|
||||||
public static final int OUTPUT_MODE_YUV = 0;
|
public static final int OUTPUT_MODE_YUV = 0;
|
||||||
public static final int OUTPUT_MODE_RGB = 1;
|
public static final int OUTPUT_MODE_RGB = 1;
|
||||||
|
public static final int OUTPUT_MODE_SURFACE_YUV = 2;
|
||||||
|
|
||||||
private static final int NO_ERROR = 0;
|
private static final int NO_ERROR = 0;
|
||||||
private static final int DECODE_ERROR = 1;
|
private static final int DECODE_ERROR = 1;
|
||||||
|
|
@ -50,10 +52,17 @@ import java.nio.ByteBuffer;
|
||||||
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
||||||
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||||
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
||||||
|
* @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
|
||||||
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
public VpxDecoder(
|
||||||
ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException {
|
int numInputBuffers,
|
||||||
|
int numOutputBuffers,
|
||||||
|
int initialInputBufferSize,
|
||||||
|
ExoMediaCrypto exoMediaCrypto,
|
||||||
|
boolean disableLoopFilter,
|
||||||
|
boolean enableSurfaceYuvOutputMode)
|
||||||
|
throws VpxDecoderException {
|
||||||
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||||
if (!VpxLibrary.isAvailable()) {
|
if (!VpxLibrary.isAvailable()) {
|
||||||
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
||||||
|
|
@ -62,7 +71,7 @@ import java.nio.ByteBuffer;
|
||||||
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
||||||
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
||||||
}
|
}
|
||||||
vpxDecContext = vpxInit(disableLoopFilter);
|
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
|
||||||
if (vpxDecContext == 0) {
|
if (vpxDecContext == 0) {
|
||||||
throw new VpxDecoderException("Failed to initialize decoder");
|
throw new VpxDecoderException("Failed to initialize decoder");
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +105,11 @@ import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
|
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
|
||||||
|
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
|
||||||
|
// require a call to vpxReleaseFrame.
|
||||||
|
if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
|
||||||
|
vpxReleaseFrame(vpxDecContext, buffer);
|
||||||
|
}
|
||||||
super.releaseOutputBuffer(buffer);
|
super.releaseOutputBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,13 +159,36 @@ import java.nio.ByteBuffer;
|
||||||
vpxClose(vpxDecContext);
|
vpxClose(vpxDecContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private native long vpxInit(boolean disableLoopFilter);
|
/** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */
|
||||||
|
public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface)
|
||||||
|
throws VpxDecoderException {
|
||||||
|
int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer);
|
||||||
|
if (getFrameResult == -1) {
|
||||||
|
throw new VpxDecoderException("Buffer render failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
|
||||||
|
|
||||||
private native long vpxClose(long context);
|
private native long vpxClose(long context);
|
||||||
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
||||||
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
|
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
|
||||||
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
|
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
|
||||||
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||||
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
|
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called
|
||||||
|
* if {@link #vpxInit} was called with {@code enableBufferManager = true}.
|
||||||
|
*/
|
||||||
|
private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link
|
||||||
|
* #vpxInit} was called with {@code enableBufferManager = true}.
|
||||||
|
*/
|
||||||
|
private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer);
|
||||||
|
|
||||||
private native int vpxGetErrorCode(long context);
|
private native int vpxGetErrorCode(long context);
|
||||||
private native String vpxGetErrorMessage(long context);
|
private native String vpxGetErrorMessage(long context);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ import java.nio.ByteBuffer;
|
||||||
public static final int COLORSPACE_BT2020 = 3;
|
public static final int COLORSPACE_BT2020 = 3;
|
||||||
|
|
||||||
private final VpxDecoder owner;
|
private final VpxDecoder owner;
|
||||||
|
/** Decoder private data. */
|
||||||
|
public int decoderPrivate;
|
||||||
|
|
||||||
public int mode;
|
public int mode;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ LOCAL_MODULE := libvpxJNI
|
||||||
LOCAL_ARM_MODE := arm
|
LOCAL_ARM_MODE := arm
|
||||||
LOCAL_CPP_EXTENSION := .cc
|
LOCAL_CPP_EXTENSION := .cc
|
||||||
LOCAL_SRC_FILES := vpx_jni.cc
|
LOCAL_SRC_FILES := vpx_jni.cc
|
||||||
LOCAL_LDLIBS := -llog -lz -lm
|
LOCAL_LDLIBS := -llog -lz -lm -landroid
|
||||||
LOCAL_SHARED_LIBRARIES := libvpx
|
LOCAL_SHARED_LIBRARIES := libvpx
|
||||||
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
|
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <android/native_window_jni.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
@ -63,6 +65,11 @@ static jmethodID initForRgbFrame;
|
||||||
static jmethodID initForYuvFrame;
|
static jmethodID initForYuvFrame;
|
||||||
static jfieldID dataField;
|
static jfieldID dataField;
|
||||||
static jfieldID outputModeField;
|
static jfieldID outputModeField;
|
||||||
|
static jfieldID decoderPrivateField;
|
||||||
|
|
||||||
|
// android.graphics.ImageFormat.YV12.
|
||||||
|
static const int kHalPixelFormatYV12 = 0x32315659;
|
||||||
|
static const int kDecoderPrivateBase = 0x100;
|
||||||
static int errorCode;
|
static int errorCode;
|
||||||
|
|
||||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
|
|
@ -282,13 +289,166 @@ static void convert_16_to_8_standard(const vpx_image_t* const img,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
struct JniFrameBuffer {
|
||||||
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
|
friend class JniBufferManager;
|
||||||
|
|
||||||
|
int stride[4];
|
||||||
|
uint8_t* planes[4];
|
||||||
|
int d_w;
|
||||||
|
int d_h;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int id;
|
||||||
|
int ref_count;
|
||||||
|
vpx_codec_frame_buffer_t vpx_fb;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JniBufferManager {
|
||||||
|
static const int MAX_FRAMES = 32;
|
||||||
|
|
||||||
|
JniFrameBuffer* all_buffers[MAX_FRAMES];
|
||||||
|
int all_buffer_count = 0;
|
||||||
|
|
||||||
|
JniFrameBuffer* free_buffers[MAX_FRAMES];
|
||||||
|
int free_buffer_count = 0;
|
||||||
|
|
||||||
|
pthread_mutex_t mutex;
|
||||||
|
|
||||||
|
public:
|
||||||
|
JniBufferManager() { pthread_mutex_init(&mutex, NULL); }
|
||||||
|
|
||||||
|
~JniBufferManager() {
|
||||||
|
while (all_buffer_count--) {
|
||||||
|
free(all_buffers[all_buffer_count]->vpx_fb.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_buffer(size_t min_size, vpx_codec_frame_buffer_t* fb) {
|
||||||
|
pthread_mutex_lock(&mutex);
|
||||||
|
JniFrameBuffer* out_buffer;
|
||||||
|
if (free_buffer_count) {
|
||||||
|
out_buffer = free_buffers[--free_buffer_count];
|
||||||
|
if (out_buffer->vpx_fb.size < min_size) {
|
||||||
|
free(out_buffer->vpx_fb.data);
|
||||||
|
out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);
|
||||||
|
out_buffer->vpx_fb.size = min_size;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out_buffer = new JniFrameBuffer();
|
||||||
|
out_buffer->id = all_buffer_count;
|
||||||
|
all_buffers[all_buffer_count++] = out_buffer;
|
||||||
|
out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);
|
||||||
|
out_buffer->vpx_fb.size = min_size;
|
||||||
|
out_buffer->vpx_fb.priv = &out_buffer->id;
|
||||||
|
}
|
||||||
|
*fb = out_buffer->vpx_fb;
|
||||||
|
int retVal = 0;
|
||||||
|
if (!out_buffer->vpx_fb.data || all_buffer_count >= MAX_FRAMES) {
|
||||||
|
LOGE("ERROR: JniBufferManager get_buffer OOM.");
|
||||||
|
retVal = -1;
|
||||||
|
} else {
|
||||||
|
memset(fb->data, 0, fb->size);
|
||||||
|
}
|
||||||
|
out_buffer->ref_count = 1;
|
||||||
|
pthread_mutex_unlock(&mutex);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
JniFrameBuffer* get_buffer(int id) const {
|
||||||
|
if (id < 0 || id >= all_buffer_count) {
|
||||||
|
LOGE("ERROR: JniBufferManager get_buffer invalid id %d.", id);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return all_buffers[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_ref(int id) {
|
||||||
|
if (id < 0 || id >= all_buffer_count) {
|
||||||
|
LOGE("ERROR: JniBufferManager add_ref invalid id %d.", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&mutex);
|
||||||
|
all_buffers[id]->ref_count++;
|
||||||
|
pthread_mutex_unlock(&mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int release(int id) {
|
||||||
|
if (id < 0 || id >= all_buffer_count) {
|
||||||
|
LOGE("ERROR: JniBufferManager release invalid id %d.", id);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&mutex);
|
||||||
|
JniFrameBuffer* buffer = all_buffers[id];
|
||||||
|
if (!buffer->ref_count) {
|
||||||
|
LOGE("ERROR: JniBufferManager release, buffer already released.");
|
||||||
|
pthread_mutex_unlock(&mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!--buffer->ref_count) {
|
||||||
|
free_buffers[free_buffer_count++] = buffer;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JniCtx {
|
||||||
|
JniCtx(bool enableBufferManager) {
|
||||||
|
if (enableBufferManager) {
|
||||||
|
buffer_manager = new JniBufferManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~JniCtx() {
|
||||||
|
if (native_window) {
|
||||||
|
ANativeWindow_release(native_window);
|
||||||
|
}
|
||||||
|
if (buffer_manager) {
|
||||||
|
delete buffer_manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void acquire_native_window(JNIEnv* env, jobject new_surface) {
|
||||||
|
if (surface != new_surface) {
|
||||||
|
if (native_window) {
|
||||||
|
ANativeWindow_release(native_window);
|
||||||
|
}
|
||||||
|
native_window = ANativeWindow_fromSurface(env, new_surface);
|
||||||
|
surface = new_surface;
|
||||||
|
width = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JniBufferManager* buffer_manager = NULL;
|
||||||
|
vpx_codec_ctx_t* decoder = NULL;
|
||||||
|
ANativeWindow* native_window = NULL;
|
||||||
|
jobject surface = NULL;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
int vpx_get_frame_buffer(void* priv, size_t min_size,
|
||||||
|
vpx_codec_frame_buffer_t* fb) {
|
||||||
|
JniBufferManager* const buffer_manager =
|
||||||
|
reinterpret_cast<JniBufferManager*>(priv);
|
||||||
|
return buffer_manager->get_buffer(min_size, fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
|
||||||
|
JniBufferManager* const buffer_manager =
|
||||||
|
reinterpret_cast<JniBufferManager*>(priv);
|
||||||
|
return buffer_manager->release(*(int*)fb->priv);
|
||||||
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
||||||
|
jboolean enableBufferManager) {
|
||||||
|
JniCtx* context = new JniCtx(enableBufferManager);
|
||||||
|
context->decoder = new vpx_codec_ctx_t();
|
||||||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||||
cfg.threads = android_getCpuCount();
|
cfg.threads = android_getCpuCount();
|
||||||
errorCode = 0;
|
errorCode = 0;
|
||||||
vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
|
vpx_codec_err_t err =
|
||||||
&cfg, 0);
|
vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);
|
||||||
if (err) {
|
if (err) {
|
||||||
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
|
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
|
||||||
errorCode = err;
|
errorCode = err;
|
||||||
|
|
@ -296,11 +456,20 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
||||||
}
|
}
|
||||||
if (disableLoopFilter) {
|
if (disableLoopFilter) {
|
||||||
// TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
|
// TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
|
||||||
err = vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true);
|
err = vpx_codec_control_(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
|
||||||
if (err) {
|
if (err) {
|
||||||
LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
|
LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (enableBufferManager) {
|
||||||
|
err = vpx_codec_set_frame_buffer_functions(
|
||||||
|
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
|
||||||
|
context->buffer_manager);
|
||||||
|
if (err) {
|
||||||
|
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
|
||||||
|
err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Populate JNI References.
|
// Populate JNI References.
|
||||||
const jclass outputBufferClass = env->FindClass(
|
const jclass outputBufferClass = env->FindClass(
|
||||||
|
|
@ -312,16 +481,17 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
||||||
dataField = env->GetFieldID(outputBufferClass, "data",
|
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||||
"Ljava/nio/ByteBuffer;");
|
"Ljava/nio/ByteBuffer;");
|
||||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||||
|
decoderPrivateField =
|
||||||
|
env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
|
||||||
return reinterpret_cast<intptr_t>(context);
|
return reinterpret_cast<intptr_t>(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
|
DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
|
||||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
const uint8_t* const buffer =
|
const uint8_t* const buffer =
|
||||||
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
|
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
|
||||||
const vpx_codec_err_t status =
|
const vpx_codec_err_t status =
|
||||||
vpx_codec_decode(context, buffer, len, NULL, 0);
|
vpx_codec_decode(context->decoder, buffer, len, NULL, 0);
|
||||||
errorCode = 0;
|
errorCode = 0;
|
||||||
if (status != VPX_CODEC_OK) {
|
if (status != VPX_CODEC_OK) {
|
||||||
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
|
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
|
||||||
|
|
@ -343,16 +513,16 @@ DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
|
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
|
||||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
vpx_codec_destroy(context);
|
vpx_codec_destroy(context->decoder);
|
||||||
delete context;
|
delete context;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
vpx_codec_iter_t iter = NULL;
|
vpx_codec_iter_t iter = NULL;
|
||||||
const vpx_image_t* const img = vpx_codec_get_frame(context, &iter);
|
const vpx_image_t* const img = vpx_codec_get_frame(context->decoder, &iter);
|
||||||
|
|
||||||
if (img == NULL) {
|
if (img == NULL) {
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -360,6 +530,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||||
|
|
||||||
const int kOutputModeYuv = 0;
|
const int kOutputModeYuv = 0;
|
||||||
const int kOutputModeRgb = 1;
|
const int kOutputModeRgb = 1;
|
||||||
|
const int kOutputModeSurfaceYuv = 2;
|
||||||
|
|
||||||
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
||||||
if (outputMode == kOutputModeRgb) {
|
if (outputMode == kOutputModeRgb) {
|
||||||
|
|
@ -435,13 +606,93 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||||
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
|
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
|
||||||
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
|
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
|
||||||
}
|
}
|
||||||
|
} else if (outputMode == kOutputModeSurfaceYuv &&
|
||||||
|
img->fmt != VPX_IMG_FMT_I42016) {
|
||||||
|
if (!context->buffer_manager) {
|
||||||
|
return -1; // enableBufferManager was not set in vpxInit.
|
||||||
|
}
|
||||||
|
int id = *(int*)img->fb_priv;
|
||||||
|
context->buffer_manager->add_ref(id);
|
||||||
|
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
|
||||||
|
for (int i = 2; i >= 0; i--) {
|
||||||
|
jfb->stride[i] = img->stride[i];
|
||||||
|
jfb->planes[i] = (uint8_t*)img->planes[i];
|
||||||
|
}
|
||||||
|
jfb->d_w = img->d_w;
|
||||||
|
jfb->d_h = img->d_h;
|
||||||
|
env->SetIntField(jOutputBuffer, decoderPrivateField,
|
||||||
|
id + kDecoderPrivateBase);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jint, vpxRenderFrame, jlong jContext, jobject jSurface,
|
||||||
|
jobject jOutputBuffer) {
|
||||||
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
|
const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -
|
||||||
|
kDecoderPrivateBase;
|
||||||
|
JniFrameBuffer* srcBuffer = context->buffer_manager->get_buffer(id);
|
||||||
|
context->acquire_native_window(env, jSurface);
|
||||||
|
if (context->native_window == NULL || !srcBuffer) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (context->width != srcBuffer->d_w || context->height != srcBuffer->d_h) {
|
||||||
|
ANativeWindow_setBuffersGeometry(context->native_window, srcBuffer->d_w,
|
||||||
|
srcBuffer->d_h, kHalPixelFormatYV12);
|
||||||
|
context->width = srcBuffer->d_w;
|
||||||
|
context->height = srcBuffer->d_h;
|
||||||
|
}
|
||||||
|
ANativeWindow_Buffer buffer;
|
||||||
|
int result = ANativeWindow_lock(context->native_window, &buffer, NULL);
|
||||||
|
if (buffer.bits == NULL || result) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Y
|
||||||
|
const size_t src_y_stride = srcBuffer->stride[VPX_PLANE_Y];
|
||||||
|
int stride = srcBuffer->d_w;
|
||||||
|
const uint8_t* src_base =
|
||||||
|
reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_Y]);
|
||||||
|
uint8_t* dest_base = (uint8_t*)buffer.bits;
|
||||||
|
for (int y = 0; y < srcBuffer->d_h; y++) {
|
||||||
|
memcpy(dest_base, src_base, stride);
|
||||||
|
src_base += src_y_stride;
|
||||||
|
dest_base += buffer.stride;
|
||||||
|
}
|
||||||
|
// UV
|
||||||
|
const int src_uv_stride = srcBuffer->stride[VPX_PLANE_U];
|
||||||
|
const int dest_uv_stride = (buffer.stride / 2 + 15) & (~15);
|
||||||
|
const int32_t buffer_uv_height = (buffer.height + 1) / 2;
|
||||||
|
const int32_t height =
|
||||||
|
std::min((int32_t)(srcBuffer->d_h + 1) / 2, buffer_uv_height);
|
||||||
|
stride = (srcBuffer->d_w + 1) / 2;
|
||||||
|
src_base = reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_U]);
|
||||||
|
const uint8_t* src_v_base =
|
||||||
|
reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_V]);
|
||||||
|
uint8_t* dest_v_base =
|
||||||
|
((uint8_t*)buffer.bits) + buffer.stride * buffer.height;
|
||||||
|
dest_base = dest_v_base + buffer_uv_height * dest_uv_stride;
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
memcpy(dest_base, src_base, stride);
|
||||||
|
memcpy(dest_v_base, src_v_base, stride);
|
||||||
|
src_base += src_uv_stride;
|
||||||
|
src_v_base += src_uv_stride;
|
||||||
|
dest_base += dest_uv_stride;
|
||||||
|
dest_v_base += dest_uv_stride;
|
||||||
|
}
|
||||||
|
return ANativeWindow_unlockAndPost(context->native_window);
|
||||||
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(void, vpxReleaseFrame, jlong jContext, jobject jOutputBuffer) {
|
||||||
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
|
const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -
|
||||||
|
kDecoderPrivateBase;
|
||||||
|
env->SetIntField(jOutputBuffer, decoderPrivateField, -1);
|
||||||
|
context->buffer_manager->release(id);
|
||||||
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
|
DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
|
||||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||||
return env->NewStringUTF(vpx_codec_error(context));
|
return env->NewStringUTF(vpx_codec_error(context->decoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }
|
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,22 @@ android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
|
// that the app's state is completely cleared between tests.
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround to prevent circular dependency on project :testutils.
|
// Workaround to prevent circular dependency on project :testutils.
|
||||||
|
|
@ -47,10 +59,14 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||||
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
|
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
|
||||||
|
androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
|
||||||
|
androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
|
||||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
testImplementation 'junit:junit:' + junitVersion
|
testImplementation 'junit:junit:' + junitVersion
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
|
|
|
||||||
BIN
library/core/src/androidTest/assets/bitmap/image_256_256.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
library/core/src/androidTest/assets/bitmap/image_80_60.bmp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4
Normal file
BIN
library/core/src/androidTest/assets/mp4/video000.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
library/core/src/androidTest/assets/mp4/video014.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
library/core/src/androidTest/assets/mp4/video015.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
library/core/src/androidTest/assets/mp4/video016.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
library/core/src/androidTest/assets/mp4/video029.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -16,8 +16,8 @@
|
||||||
package com.google.android.exoplayer2.upstream;
|
package com.google.android.exoplayer2.upstream;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static junit.framework.Assert.fail;
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
|
||||||
import android.content.ContentProvider;
|
import android.content.ContentProvider;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
|
@ -28,48 +28,58 @@ import android.os.Bundle;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.test.InstrumentationTestCase;
|
import androidx.test.InstrumentationRegistry;
|
||||||
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/**
|
/** Unit tests for {@link ContentDataSource}. */
|
||||||
* Unit tests for {@link ContentDataSource}.
|
@RunWith(AndroidJUnit4.class)
|
||||||
*/
|
public final class ContentDataSourceTest {
|
||||||
public final class ContentDataSourceTest extends InstrumentationTestCase {
|
|
||||||
|
|
||||||
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
|
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
|
||||||
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
|
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testRead() throws Exception {
|
public void testRead() throws Exception {
|
||||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false);
|
assertData(0, C.LENGTH_UNSET, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadPipeMode() throws Exception {
|
public void testReadPipeMode() throws Exception {
|
||||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true);
|
assertData(0, C.LENGTH_UNSET, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadFixedLength() throws Exception {
|
public void testReadFixedLength() throws Exception {
|
||||||
assertData(getInstrumentation(), 0, 100, false);
|
assertData(0, 100, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadFromOffsetToEndOfInput() throws Exception {
|
public void testReadFromOffsetToEndOfInput() throws Exception {
|
||||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false);
|
assertData(1, C.LENGTH_UNSET, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
|
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
|
||||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true);
|
assertData(1, C.LENGTH_UNSET, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadFromOffsetFixedLength() throws Exception {
|
public void testReadFromOffsetFixedLength() throws Exception {
|
||||||
assertData(getInstrumentation(), 1, 100, false);
|
assertData(1, 100, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadInvalidUri() throws Exception {
|
public void testReadInvalidUri() throws Exception {
|
||||||
ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
|
ContentDataSource dataSource =
|
||||||
|
new ContentDataSource(InstrumentationRegistry.getTargetContext());
|
||||||
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
|
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
|
||||||
DataSpec dataSpec = new DataSpec(contentUri);
|
DataSpec dataSpec = new DataSpec(contentUri);
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertData(Instrumentation instrumentation, int offset, int length,
|
private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
|
||||||
boolean pipeMode) throws IOException {
|
|
||||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
|
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
|
||||||
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
|
ContentDataSource dataSource =
|
||||||
|
new ContentDataSource(InstrumentationRegistry.getTargetContext());
|
||||||
try {
|
try {
|
||||||
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
|
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
|
||||||
byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
|
byte[] completeData =
|
||||||
|
TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);
|
||||||
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
|
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
|
||||||
length == C.LENGTH_UNSET ? completeData.length : offset + length);
|
length == C.LENGTH_UNSET ? completeData.length : offset + length);
|
||||||
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
|
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.test.InstrumentationTestCase;
|
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
import androidx.test.InstrumentationRegistry;
|
||||||
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -29,9 +30,14 @@ import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Tests {@link CachedContentIndex}. */
|
/** Tests {@link CachedContentIndex}. */
|
||||||
public class CachedContentIndexTest extends InstrumentationTestCase {
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class CachedContentIndexTest {
|
||||||
|
|
||||||
private final byte[] testIndexV1File = {
|
private final byte[] testIndexV1File = {
|
||||||
0, 0, 0, 1, // version
|
0, 0, 0, 1, // version
|
||||||
|
|
@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
private CachedContentIndex index;
|
private CachedContentIndex index;
|
||||||
private File cacheDir;
|
private File cacheDir;
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
super.setUp();
|
cacheDir =
|
||||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||||
index = new CachedContentIndex(cacheDir);
|
index = new CachedContentIndex(cacheDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@After
|
||||||
protected void tearDown() throws Exception {
|
public void tearDown() {
|
||||||
Util.recursiveDelete(cacheDir);
|
Util.recursiveDelete(cacheDir);
|
||||||
super.tearDown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testAddGetRemove() throws Exception {
|
public void testAddGetRemove() throws Exception {
|
||||||
final String key1 = "key1";
|
final String key1 = "key1";
|
||||||
final String key2 = "key2";
|
final String key2 = "key2";
|
||||||
|
|
@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(cacheSpanFile.exists()).isTrue();
|
assertThat(cacheSpanFile.exists()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testStoreAndLoad() throws Exception {
|
public void testStoreAndLoad() throws Exception {
|
||||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
|
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testLoadV1() throws Exception {
|
public void testLoadV1() throws Exception {
|
||||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||||
fos.write(testIndexV1File);
|
fos.write(testIndexV1File);
|
||||||
|
|
@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testLoadV2() throws Exception {
|
public void testLoadV2() throws Exception {
|
||||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||||
fos.write(testIndexV2File);
|
fos.write(testIndexV2File);
|
||||||
|
|
@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
|
@Test
|
||||||
|
public void testAssignIdForKeyAndGetKeyForId() {
|
||||||
final String key1 = "key1";
|
final String key1 = "key1";
|
||||||
final String key2 = "key2";
|
final String key2 = "key2";
|
||||||
int id1 = index.assignIdForKey(key1);
|
int id1 = index.assignIdForKey(key1);
|
||||||
|
|
@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
|
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGetNewId() throws Exception {
|
@Test
|
||||||
|
public void testGetNewId() {
|
||||||
SparseArray<String> idToKey = new SparseArray<>();
|
SparseArray<String> idToKey = new SparseArray<>();
|
||||||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
|
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
|
||||||
idToKey.put(10, "");
|
idToKey.put(10, "");
|
||||||
|
|
@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
|
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testEncryption() throws Exception {
|
public void testEncryption() throws Exception {
|
||||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||||
|
|
@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
|
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testRemoveEmptyNotLockedCachedContent() throws Exception {
|
@Test
|
||||||
|
public void testRemoveEmptyNotLockedCachedContent() {
|
||||||
CachedContent cachedContent = index.getOrAdd("key1");
|
CachedContent cachedContent = index.getOrAdd("key1");
|
||||||
|
|
||||||
index.maybeRemove(cachedContent.key);
|
index.maybeRemove(cachedContent.key);
|
||||||
|
|
@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(index.get(cachedContent.key)).isNull();
|
assertThat(index.get(cachedContent.key)).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testCantRemoveNotEmptyCachedContent() throws Exception {
|
public void testCantRemoveNotEmptyCachedContent() throws Exception {
|
||||||
CachedContent cachedContent = index.getOrAdd("key1");
|
CachedContent cachedContent = index.getOrAdd("key1");
|
||||||
File cacheSpanFile =
|
File cacheSpanFile =
|
||||||
|
|
@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||||
assertThat(index.get(cachedContent.key)).isNotNull();
|
assertThat(index.get(cachedContent.key)).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testCantRemoveLockedCachedContent() throws Exception {
|
@Test
|
||||||
|
public void testCantRemoveLockedCachedContent() {
|
||||||
CachedContent cachedContent = index.getOrAdd("key1");
|
CachedContent cachedContent = index.getOrAdd("key1");
|
||||||
cachedContent.setLocked(true);
|
cachedContent.setLocked(true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import androidx.test.InstrumentationRegistry;
|
||||||
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
|
@ -26,11 +27,14 @@ import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/**
|
/** Unit tests for {@link SimpleCacheSpan}. */
|
||||||
* Unit tests for {@link SimpleCacheSpan}.
|
@RunWith(AndroidJUnit4.class)
|
||||||
*/
|
public class SimpleCacheSpanTest {
|
||||||
public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
|
||||||
|
|
||||||
private CachedContentIndex index;
|
private CachedContentIndex index;
|
||||||
private File cacheDir;
|
private File cacheDir;
|
||||||
|
|
@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||||
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
|
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
protected void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
super.setUp();
|
cacheDir =
|
||||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||||
index = new CachedContentIndex(cacheDir);
|
index = new CachedContentIndex(cacheDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@After
|
||||||
protected void tearDown() throws Exception {
|
public void tearDown() {
|
||||||
Util.recursiveDelete(cacheDir);
|
Util.recursiveDelete(cacheDir);
|
||||||
super.tearDown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testCacheFile() throws Exception {
|
public void testCacheFile() throws Exception {
|
||||||
assertCacheSpan("key1", 0, 0);
|
assertCacheSpan("key1", 0, 0);
|
||||||
assertCacheSpan("key2", 1, 2);
|
assertCacheSpan("key2", 1, 2);
|
||||||
|
|
@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||||
+ "A paragraph-separator character \u2029", 1, 2);
|
+ "A paragraph-separator character \u2029", 1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testUpgradeFileName() throws Exception {
|
public void testUpgradeFileName() throws Exception {
|
||||||
String key = "asd\u00aa";
|
String key = "asd\u00aa";
|
||||||
int id = index.assignIdForKey(key);
|
int id = index.assignIdForKey(key);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import android.media.MediaFormat;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||||
|
import com.google.android.exoplayer2.audio.AuxEffectInfo;
|
||||||
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;
|
||||||
|
|
@ -77,6 +78,12 @@ public final class C {
|
||||||
*/
|
*/
|
||||||
public static final long NANOS_PER_SECOND = 1000000000L;
|
public static final long NANOS_PER_SECOND = 1000000000L;
|
||||||
|
|
||||||
|
/** The number of bits per byte. */
|
||||||
|
public static final int BITS_PER_BYTE = 8;
|
||||||
|
|
||||||
|
/** The number of bytes per float. */
|
||||||
|
public static final int BYTES_PER_FLOAT = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the ASCII charset.
|
* The name of the ASCII charset.
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,6 +143,8 @@ public final class C {
|
||||||
ENCODING_PCM_24BIT,
|
ENCODING_PCM_24BIT,
|
||||||
ENCODING_PCM_32BIT,
|
ENCODING_PCM_32BIT,
|
||||||
ENCODING_PCM_FLOAT,
|
ENCODING_PCM_FLOAT,
|
||||||
|
ENCODING_PCM_MU_LAW,
|
||||||
|
ENCODING_PCM_A_LAW,
|
||||||
ENCODING_AC3,
|
ENCODING_AC3,
|
||||||
ENCODING_E_AC3,
|
ENCODING_E_AC3,
|
||||||
ENCODING_DTS,
|
ENCODING_DTS,
|
||||||
|
|
@ -144,12 +153,19 @@ public final class C {
|
||||||
})
|
})
|
||||||
public @interface Encoding {}
|
public @interface Encoding {}
|
||||||
|
|
||||||
/**
|
/** Represents a PCM audio encoding, or an invalid or unset value. */
|
||||||
* Represents a PCM audio encoding, or an invalid or unset value.
|
|
||||||
*/
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
|
@IntDef({
|
||||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
|
Format.NO_VALUE,
|
||||||
|
ENCODING_INVALID,
|
||||||
|
ENCODING_PCM_8BIT,
|
||||||
|
ENCODING_PCM_16BIT,
|
||||||
|
ENCODING_PCM_24BIT,
|
||||||
|
ENCODING_PCM_32BIT,
|
||||||
|
ENCODING_PCM_FLOAT,
|
||||||
|
ENCODING_PCM_MU_LAW,
|
||||||
|
ENCODING_PCM_A_LAW
|
||||||
|
})
|
||||||
public @interface PcmEncoding {}
|
public @interface PcmEncoding {}
|
||||||
/** @see AudioFormat#ENCODING_INVALID */
|
/** @see AudioFormat#ENCODING_INVALID */
|
||||||
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
|
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
|
||||||
|
|
@ -163,6 +179,10 @@ public final class C {
|
||||||
public static final int ENCODING_PCM_32BIT = 0x40000000;
|
public static final int ENCODING_PCM_32BIT = 0x40000000;
|
||||||
/** @see AudioFormat#ENCODING_PCM_FLOAT */
|
/** @see AudioFormat#ENCODING_PCM_FLOAT */
|
||||||
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
|
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
|
||||||
|
/** Audio encoding for mu-law. */
|
||||||
|
public static final int ENCODING_PCM_MU_LAW = 0x10000000;
|
||||||
|
/** Audio encoding for A-law. */
|
||||||
|
public static final int ENCODING_PCM_A_LAW = 0x20000000;
|
||||||
/** @see AudioFormat#ENCODING_AC3 */
|
/** @see AudioFormat#ENCODING_AC3 */
|
||||||
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
||||||
/** @see AudioFormat#ENCODING_E_AC3 */
|
/** @see AudioFormat#ENCODING_E_AC3 */
|
||||||
|
|
@ -174,13 +194,6 @@ public final class C {
|
||||||
/** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
|
/** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
|
||||||
public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
|
public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
|
||||||
|
|
||||||
/**
|
|
||||||
* @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
|
|
||||||
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream types for an {@link android.media.AudioTrack}.
|
* Stream types for an {@link android.media.AudioTrack}.
|
||||||
*/
|
*/
|
||||||
|
|
@ -271,24 +284,32 @@ public final class C {
|
||||||
public static final int FLAG_AUDIBILITY_ENFORCED =
|
public static final int FLAG_AUDIBILITY_ENFORCED =
|
||||||
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
|
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
|
||||||
|
|
||||||
/**
|
/** Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. */
|
||||||
* Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
|
|
||||||
*/
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
|
@IntDef({
|
||||||
USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION,
|
USAGE_ALARM,
|
||||||
USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
|
USAGE_ASSISTANCE_ACCESSIBILITY,
|
||||||
USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT,
|
USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
|
||||||
USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION,
|
USAGE_ASSISTANCE_SONIFICATION,
|
||||||
USAGE_VOICE_COMMUNICATION_SIGNALLING})
|
USAGE_ASSISTANT,
|
||||||
|
USAGE_GAME,
|
||||||
|
USAGE_MEDIA,
|
||||||
|
USAGE_NOTIFICATION,
|
||||||
|
USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
|
||||||
|
USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
|
||||||
|
USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
|
||||||
|
USAGE_NOTIFICATION_EVENT,
|
||||||
|
USAGE_NOTIFICATION_RINGTONE,
|
||||||
|
USAGE_UNKNOWN,
|
||||||
|
USAGE_VOICE_COMMUNICATION,
|
||||||
|
USAGE_VOICE_COMMUNICATION_SIGNALLING
|
||||||
|
})
|
||||||
public @interface AudioUsage {}
|
public @interface AudioUsage {}
|
||||||
/**
|
/**
|
||||||
* @see android.media.AudioAttributes#USAGE_ALARM
|
* @see android.media.AudioAttributes#USAGE_ALARM
|
||||||
*/
|
*/
|
||||||
public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
|
public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
|
||||||
/**
|
/** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */
|
||||||
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
|
|
||||||
*/
|
|
||||||
public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
|
public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
|
||||||
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
|
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
|
||||||
/**
|
/**
|
||||||
|
|
@ -301,6 +322,8 @@ public final class C {
|
||||||
*/
|
*/
|
||||||
public static final int USAGE_ASSISTANCE_SONIFICATION =
|
public static final int USAGE_ASSISTANCE_SONIFICATION =
|
||||||
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
|
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
|
||||||
|
/** @see android.media.AudioAttributes#USAGE_ASSISTANT */
|
||||||
|
public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;
|
||||||
/**
|
/**
|
||||||
* @see android.media.AudioAttributes#USAGE_GAME
|
* @see android.media.AudioAttributes#USAGE_GAME
|
||||||
*/
|
*/
|
||||||
|
|
@ -353,6 +376,29 @@ public final class C {
|
||||||
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
|
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
|
||||||
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
|
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
|
||||||
|
|
||||||
|
/** Audio focus types. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({
|
||||||
|
AUDIOFOCUS_NONE,
|
||||||
|
AUDIOFOCUS_GAIN,
|
||||||
|
AUDIOFOCUS_GAIN_TRANSIENT,
|
||||||
|
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
|
||||||
|
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
||||||
|
})
|
||||||
|
public @interface AudioFocusGain {}
|
||||||
|
/** @see AudioManager#AUDIOFOCUS_NONE */
|
||||||
|
public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE;
|
||||||
|
/** @see AudioManager#AUDIOFOCUS_GAIN */
|
||||||
|
public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN;
|
||||||
|
/** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */
|
||||||
|
public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
|
||||||
|
/** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */
|
||||||
|
public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK =
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
|
||||||
|
/** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */
|
||||||
|
public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE =
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags which can apply to a buffer containing a media sample.
|
* Flags which can apply to a buffer containing a media sample.
|
||||||
*/
|
*/
|
||||||
|
|
@ -368,14 +414,10 @@ public final class C {
|
||||||
* Flag for empty buffers that signal that the end of the stream was reached.
|
* Flag for empty buffers that signal that the end of the stream was reached.
|
||||||
*/
|
*/
|
||||||
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||||
/**
|
/** Indicates that a buffer is (at least partially) encrypted. */
|
||||||
* Indicates that a buffer is (at least partially) encrypted.
|
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
||||||
*/
|
/** Indicates that a buffer should be decoded but not rendered. */
|
||||||
public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
|
public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000
|
||||||
/**
|
|
||||||
* Indicates that a buffer should be decoded but not rendered.
|
|
||||||
*/
|
|
||||||
public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
|
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
|
||||||
|
|
@ -409,15 +451,13 @@ public final class C {
|
||||||
* Indicates that the track should be selected if user preferences do not state otherwise.
|
* Indicates that the track should be selected if user preferences do not state otherwise.
|
||||||
*/
|
*/
|
||||||
public static final int SELECTION_FLAG_DEFAULT = 1;
|
public static final int SELECTION_FLAG_DEFAULT = 1;
|
||||||
/**
|
/** Indicates that the track must be displayed. Only applies to text tracks. */
|
||||||
* Indicates that the track must be displayed. Only applies to text tracks.
|
public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2
|
||||||
*/
|
|
||||||
public static final int SELECTION_FLAG_FORCED = 2;
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the player may choose to play the track in absence of an explicit user
|
* Indicates that the player may choose to play the track in absence of an explicit user
|
||||||
* preference.
|
* preference.
|
||||||
*/
|
*/
|
||||||
public static final int SELECTION_FLAG_AUTOSELECT = 4;
|
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an undetermined language as an ISO 639 alpha-3 language code.
|
* Represents an undetermined language as an ISO 639 alpha-3 language code.
|
||||||
|
|
@ -469,32 +509,24 @@ public final class C {
|
||||||
*/
|
*/
|
||||||
public static final int RESULT_FORMAT_READ = -5;
|
public static final int RESULT_FORMAT_READ = -5;
|
||||||
|
|
||||||
/**
|
/** A data type constant for data of unknown or unspecified type. */
|
||||||
* A data type constant for data of unknown or unspecified type.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_UNKNOWN = 0;
|
public static final int DATA_TYPE_UNKNOWN = 0;
|
||||||
/**
|
/** A data type constant for media, typically containing media samples. */
|
||||||
* A data type constant for media, typically containing media samples.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_MEDIA = 1;
|
public static final int DATA_TYPE_MEDIA = 1;
|
||||||
/**
|
/** A data type constant for media, typically containing only initialization data. */
|
||||||
* A data type constant for media, typically containing only initialization data.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
|
public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
|
||||||
/**
|
/** A data type constant for drm or encryption data. */
|
||||||
* A data type constant for drm or encryption data.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_DRM = 3;
|
public static final int DATA_TYPE_DRM = 3;
|
||||||
/**
|
/** A data type constant for a manifest file. */
|
||||||
* A data type constant for a manifest file.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_MANIFEST = 4;
|
public static final int DATA_TYPE_MANIFEST = 4;
|
||||||
/**
|
/** A data type constant for time synchronization data. */
|
||||||
* A data type constant for time synchronization data.
|
|
||||||
*/
|
|
||||||
public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
|
public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
|
||||||
/** A data type constant for ads loader data. */
|
/** A data type constant for ads loader data. */
|
||||||
public static final int DATA_TYPE_AD = 6;
|
public static final int DATA_TYPE_AD = 6;
|
||||||
|
/**
|
||||||
|
* A data type constant for live progressive media streams, typically containing media samples.
|
||||||
|
*/
|
||||||
|
public static final int DATA_TYPE_MEDIA_PROGRESSIVE_LIVE = 7;
|
||||||
/**
|
/**
|
||||||
* Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
|
* Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
|
||||||
* equal to this value.
|
* equal to this value.
|
||||||
|
|
@ -694,6 +726,13 @@ public final class C {
|
||||||
*/
|
*/
|
||||||
public static final int MSG_SET_SCALING_MODE = 4;
|
public static final int MSG_SET_SCALING_MODE = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of a message that can be passed to an audio {@link Renderer} via {@link
|
||||||
|
* ExoPlayer#createMessage(Target)}. The message payload should be an {@link AuxEffectInfo}
|
||||||
|
* instance representing an auxiliary audio effect for the underlying audio track.
|
||||||
|
*/
|
||||||
|
public static final int MSG_SET_AUX_EFFECT_INFO = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
|
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
|
||||||
* {@link Renderer}s. These custom constants must be greater than or equal to this value.
|
* {@link Renderer}s. These custom constants must be greater than or equal to this value.
|
||||||
|
|
@ -797,6 +836,45 @@ public final class C {
|
||||||
*/
|
*/
|
||||||
public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;
|
public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;
|
||||||
|
|
||||||
|
/** Network connection type. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({
|
||||||
|
NETWORK_TYPE_UNKNOWN,
|
||||||
|
NETWORK_TYPE_OFFLINE,
|
||||||
|
NETWORK_TYPE_WIFI,
|
||||||
|
NETWORK_TYPE_2G,
|
||||||
|
NETWORK_TYPE_3G,
|
||||||
|
NETWORK_TYPE_4G,
|
||||||
|
NETWORK_TYPE_CELLULAR_UNKNOWN,
|
||||||
|
NETWORK_TYPE_ETHERNET,
|
||||||
|
NETWORK_TYPE_OTHER
|
||||||
|
})
|
||||||
|
public @interface NetworkType {}
|
||||||
|
/** Unknown network type. */
|
||||||
|
public static final int NETWORK_TYPE_UNKNOWN = 0;
|
||||||
|
/** No network connection. */
|
||||||
|
public static final int NETWORK_TYPE_OFFLINE = 1;
|
||||||
|
/** Network type for a Wifi connection. */
|
||||||
|
public static final int NETWORK_TYPE_WIFI = 2;
|
||||||
|
/** Network type for a 2G cellular connection. */
|
||||||
|
public static final int NETWORK_TYPE_2G = 3;
|
||||||
|
/** Network type for a 3G cellular connection. */
|
||||||
|
public static final int NETWORK_TYPE_3G = 4;
|
||||||
|
/** Network type for a 4G cellular connection. */
|
||||||
|
public static final int NETWORK_TYPE_4G = 5;
|
||||||
|
/**
|
||||||
|
* Network type for cellular connections which cannot be mapped to one of {@link
|
||||||
|
* #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
|
||||||
|
*/
|
||||||
|
public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
|
||||||
|
/** Network type for an Ethernet connection. */
|
||||||
|
public static final int NETWORK_TYPE_ETHERNET = 7;
|
||||||
|
/**
|
||||||
|
* Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
|
||||||
|
* Bluetooth).
|
||||||
|
*/
|
||||||
|
public static final int NETWORK_TYPE_OTHER = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
|
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
|
||||||
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
|
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,13 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||||
* model">
|
* model">
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>It is strongly recommended that ExoPlayer instances are created and accessed from a single
|
* <li>ExoPlayer instances must be accessed from the thread associated with {@link
|
||||||
* application thread. The application's main thread is ideal. Accessing an instance from
|
* #getApplicationLooper()}. This Looper can be specified when creating the player, or this is
|
||||||
* multiple threads is discouraged as it may cause synchronization problems.
|
* the Looper of the thread the player is created on, or the Looper of the application's main
|
||||||
* <li>Registered listeners are called on the thread that created the ExoPlayer instance, unless
|
* thread if the player is created on a thread without Looper.
|
||||||
* the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
|
* <li>Registered listeners are called on the thread associated with {@link
|
||||||
* case, registered listeners will be called on the application's main thread.
|
* #getApplicationLooper()}. Note that this means registered listeners are called on the same
|
||||||
|
* thread which must be used to access the player.
|
||||||
* <li>An internal playback thread is responsible for playback. Injected player components such as
|
* <li>An internal playback thread is responsible for playback. Injected player components such as
|
||||||
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
|
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
|
||||||
* thread.
|
* thread.
|
||||||
|
|
@ -178,13 +179,15 @@ public interface ExoPlayer extends Player {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
|
@RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
|
||||||
|
|
||||||
/**
|
/** Returns the {@link Looper} associated with the playback thread. */
|
||||||
* Gets the {@link Looper} associated with the playback thread.
|
|
||||||
*
|
|
||||||
* @return The {@link Looper} associated with the playback thread.
|
|
||||||
*/
|
|
||||||
Looper getPlaybackLooper();
|
Looper getPlaybackLooper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link Looper} associated with the application thread that's used to access the
|
||||||
|
* player and on which player events are received.
|
||||||
|
*/
|
||||||
|
Looper getApplicationLooper();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
|
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
|
||||||
* {@code prepare(mediaSource, true, true)}.
|
* {@code prepare(mediaSource, true, true)}.
|
||||||
|
|
@ -239,4 +242,7 @@ public interface ExoPlayer extends Player {
|
||||||
* @param seekParameters The seek parameters, or {@code null} to use the defaults.
|
* @param seekParameters The seek parameters, or {@code null} to use the defaults.
|
||||||
*/
|
*/
|
||||||
void setSeekParameters(@Nullable SeekParameters seekParameters);
|
void setSeekParameters(@Nullable SeekParameters seekParameters);
|
||||||
|
|
||||||
|
/** Returns the currently active {@link SeekParameters} of the player. */
|
||||||
|
SeekParameters getSeekParameters();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,25 @@
|
||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Looper;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||||
|
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory for {@link ExoPlayer} instances.
|
* A factory for {@link ExoPlayer} instances.
|
||||||
*/
|
*/
|
||||||
public final class ExoPlayerFactory {
|
public final class ExoPlayerFactory {
|
||||||
|
|
||||||
|
private static @Nullable BandwidthMeter singletonBandwidthMeter;
|
||||||
|
|
||||||
private ExoPlayerFactory() {}
|
private ExoPlayerFactory() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -36,13 +43,14 @@ public final class ExoPlayerFactory {
|
||||||
* @param context A {@link Context}.
|
* @param context A {@link Context}.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||||
|
* LoadControl)}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
LoadControl loadControl) {
|
Context context, TrackSelector trackSelector, LoadControl loadControl) {
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||||
return newSimpleInstance(renderersFactory, trackSelector, loadControl);
|
return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,14 +61,18 @@ public final class ExoPlayerFactory {
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
* will not be used for DRM protected playbacks.
|
* will not be used for DRM protected playbacks.
|
||||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||||
|
* LoadControl)}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
|
TrackSelector trackSelector,
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||||
return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
|
return newSimpleInstance(
|
||||||
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -74,14 +86,19 @@ public final class ExoPlayerFactory {
|
||||||
* @param extensionRendererMode The extension renderer mode, which determines if and how available
|
* @param extensionRendererMode The extension renderer mode, which determines if and how available
|
||||||
* extension renderers are used. Note that extensions must be included in the application
|
* extension renderers are used. Note that extensions must be included in the application
|
||||||
* build for them to be considered available.
|
* build for them to be considered available.
|
||||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||||
|
* LoadControl)}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
LoadControl loadControl, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
Context context,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
|
||||||
return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
|
return newSimpleInstance(
|
||||||
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,16 +114,21 @@ public final class ExoPlayerFactory {
|
||||||
* build for them to be considered available.
|
* build for them to be considered available.
|
||||||
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
|
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
|
||||||
* seamlessly join an ongoing playback.
|
* seamlessly join an ongoing playback.
|
||||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||||
|
* LoadControl)}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
LoadControl loadControl, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
Context context,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
||||||
long allowedVideoJoiningTimeMs) {
|
long allowedVideoJoiningTimeMs) {
|
||||||
RenderersFactory renderersFactory =
|
RenderersFactory renderersFactory =
|
||||||
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
|
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
|
||||||
return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
|
return newSimpleInstance(
|
||||||
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,7 +138,7 @@ public final class ExoPlayerFactory {
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {
|
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {
|
||||||
return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector);
|
return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -124,44 +146,74 @@ public final class ExoPlayerFactory {
|
||||||
*
|
*
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use
|
||||||
|
* of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio
|
||||||
|
* focus will be unavailable for the {@link SimpleExoPlayer} returned by this method.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
|
@Deprecated
|
||||||
TrackSelector trackSelector) {
|
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||||
return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
RenderersFactory renderersFactory, TrackSelector trackSelector) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
/* context= */ null, renderersFactory, trackSelector, new DefaultLoadControl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link SimpleExoPlayer} instance.
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
*
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
*/
|
||||||
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) {
|
||||||
|
return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
* will not be used for DRM protected playbacks.
|
* will not be used for DRM protected playbacks.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
RenderersFactory renderersFactory,
|
RenderersFactory renderersFactory,
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||||
return newSimpleInstance(
|
return newSimpleInstance(
|
||||||
renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
|
context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link SimpleExoPlayer} instance.
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
*
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
TrackSelector trackSelector, LoadControl loadControl) {
|
Context context,
|
||||||
return new SimpleExoPlayer(
|
RenderersFactory renderersFactory,
|
||||||
renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null);
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
/* drmSessionManager= */ null,
|
||||||
|
Util.getLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link SimpleExoPlayer} instance.
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
*
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
|
@ -169,16 +221,48 @@ public final class ExoPlayerFactory {
|
||||||
* will not be used for DRM protected playbacks.
|
* will not be used for DRM protected playbacks.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
RenderersFactory renderersFactory,
|
RenderersFactory renderersFactory,
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||||
return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager);
|
return newSimpleInstance(
|
||||||
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link SimpleExoPlayer} instance.
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
*
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
|
* will not be used for DRM protected playbacks.
|
||||||
|
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
||||||
|
*/
|
||||||
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
BandwidthMeter bandwidthMeter) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
drmSessionManager,
|
||||||
|
bandwidthMeter,
|
||||||
|
new AnalyticsCollector.Factory(),
|
||||||
|
Util.getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
|
@ -188,13 +272,116 @@ public final class ExoPlayerFactory {
|
||||||
* will collect and forward all player events.
|
* will collect and forward all player events.
|
||||||
*/
|
*/
|
||||||
public static SimpleExoPlayer newSimpleInstance(
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
RenderersFactory renderersFactory,
|
RenderersFactory renderersFactory,
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
AnalyticsCollector.Factory analyticsCollectorFactory) {
|
AnalyticsCollector.Factory analyticsCollectorFactory) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
drmSessionManager,
|
||||||
|
analyticsCollectorFactory,
|
||||||
|
Util.getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
|
* will not be used for DRM protected playbacks.
|
||||||
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
* used to call listeners on.
|
||||||
|
*/
|
||||||
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
Looper looper) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
drmSessionManager,
|
||||||
|
new AnalyticsCollector.Factory(),
|
||||||
|
looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
|
* will not be used for DRM protected playbacks.
|
||||||
|
* @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
|
||||||
|
* will collect and forward all player events.
|
||||||
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
* used to call listeners on.
|
||||||
|
*/
|
||||||
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
AnalyticsCollector.Factory analyticsCollectorFactory,
|
||||||
|
Looper looper) {
|
||||||
|
return newSimpleInstance(
|
||||||
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
drmSessionManager,
|
||||||
|
getDefaultBandwidthMeter(),
|
||||||
|
analyticsCollectorFactory,
|
||||||
|
looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SimpleExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||||
|
* will not be used for DRM protected playbacks.
|
||||||
|
* @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
|
||||||
|
* will collect and forward all player events.
|
||||||
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
* used to call listeners on.
|
||||||
|
*/
|
||||||
|
public static SimpleExoPlayer newSimpleInstance(
|
||||||
|
Context context,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
BandwidthMeter bandwidthMeter,
|
||||||
|
AnalyticsCollector.Factory analyticsCollectorFactory,
|
||||||
|
Looper looper) {
|
||||||
return new SimpleExoPlayer(
|
return new SimpleExoPlayer(
|
||||||
renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory);
|
context,
|
||||||
|
renderersFactory,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
drmSessionManager,
|
||||||
|
bandwidthMeter,
|
||||||
|
analyticsCollectorFactory,
|
||||||
|
looper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -216,7 +403,47 @@ public final class ExoPlayerFactory {
|
||||||
*/
|
*/
|
||||||
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
|
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
|
||||||
LoadControl loadControl) {
|
LoadControl loadControl) {
|
||||||
return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT);
|
return newInstance(renderers, trackSelector, loadControl, Util.getLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link ExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param renderers The {@link Renderer}s that will be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
* used to call listeners on.
|
||||||
|
*/
|
||||||
|
public static ExoPlayer newInstance(
|
||||||
|
Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) {
|
||||||
|
return newInstance(renderers, trackSelector, loadControl, getDefaultBandwidthMeter(), looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link ExoPlayer} instance.
|
||||||
|
*
|
||||||
|
* @param renderers The {@link Renderer}s that will be used by the instance.
|
||||||
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
|
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
||||||
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
* used to call listeners on.
|
||||||
|
*/
|
||||||
|
public static ExoPlayer newInstance(
|
||||||
|
Renderer[] renderers,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
LoadControl loadControl,
|
||||||
|
BandwidthMeter bandwidthMeter,
|
||||||
|
Looper looper) {
|
||||||
|
return new ExoPlayerImpl(
|
||||||
|
renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized BandwidthMeter getDefaultBandwidthMeter() {
|
||||||
|
if (singletonBandwidthMeter == null) {
|
||||||
|
singletonBandwidthMeter = new DefaultBandwidthMeter.Builder().build();
|
||||||
|
}
|
||||||
|
return singletonBandwidthMeter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||