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) ###
|
||||
|
||||
* 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
|
||||
`InvalidResponseCodeException`s.
|
||||
* Add possibility to cancel `PlayerMessage`s.
|
||||
|
|
@ -19,10 +210,7 @@
|
|||
([#4228](https://github.com/google/ExoPlayer/issues/4228)).
|
||||
* FLAC: Supports seeking for FLAC files without SEEKTABLE
|
||||
([#1808](https://github.com/google/ExoPlayer/issues/1808)).
|
||||
* HLS:
|
||||
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
|
||||
([#4239](https://github.com/google/ExoPlayer/issues/4239)).
|
||||
* Caption:
|
||||
* Captions:
|
||||
* TTML:
|
||||
* 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.
|
||||
|
|
@ -57,7 +245,7 @@
|
|||
periods are created, released and being read from.
|
||||
* Support live stream clipping with `ClippingMediaSource`.
|
||||
* 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:
|
||||
* Add support for displaying error messages and a buffering spinner in
|
||||
`PlayerView`.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
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.google.android.gms:strict-version-matcher-plugin:1.0.3'
|
||||
}
|
||||
// Workaround for the following test coverage issue. Remove when fixed:
|
||||
// https://code.google.com/p/android/issues/detail?id=226070
|
||||
|
|
|
|||
|
|
@ -13,19 +13,18 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.8.0'
|
||||
releaseVersionCode = 2800
|
||||
releaseVersion = '2.8.4'
|
||||
releaseVersionCode = 2804
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
// by the library requires API level 16 or greater.
|
||||
minSdkVersion = 14
|
||||
targetSdkVersion = 27
|
||||
compileSdkVersion = 27
|
||||
buildToolsVersion = '27.0.3'
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
buildToolsVersion = '28.0.2'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
playServicesLibraryVersion = '12.0.0'
|
||||
supportLibraryVersion = '27.1.1'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
junitVersion = '4.12'
|
||||
|
|
@ -33,6 +32,7 @@ project.ext {
|
|||
robolectricVersion = '3.7.1'
|
||||
autoValueVersion = '1.6'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
testRunnerVersion = '1.1.0-alpha3'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ include modulePrefix + 'extension-flac'
|
|||
include modulePrefix + 'extension-gvr'
|
||||
include modulePrefix + 'extension-ima'
|
||||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-cronet'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
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-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||
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-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
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-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
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
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
|
|
@ -57,3 +62,5 @@ dependencies {
|
|||
implementation 'com.android.support:appcompat-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.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
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.EventListener;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
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.MediaSource;
|
||||
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.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
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.gms.cast.MediaInfo;
|
||||
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 java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Manages players and an internal media queue for the ExoPlayer/Cast demo app.
|
||||
*/
|
||||
/* package */ final class PlayerManager extends DefaultEventListener
|
||||
implements CastPlayer.SessionAvailabilityListener {
|
||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
||||
/* package */ final class PlayerManager
|
||||
implements EventListener, CastPlayer.SessionAvailabilityListener {
|
||||
|
||||
/**
|
||||
* 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 DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||
|
||||
private final PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
|
|
@ -119,9 +114,9 @@ import java.util.ArrayList;
|
|||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
|
|
@ -282,7 +277,7 @@ import java.util.ArrayList;
|
|||
|
||||
@Override
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
if (timeline.isEmpty()) {
|
||||
castMediaQueueCreationPending = true;
|
||||
|
|
@ -396,13 +391,9 @@ import java.util.ArrayList;
|
|||
Uri uri = Uri.parse(sample.uri);
|
||||
switch (sample.mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
|
|
@ -51,3 +56,5 @@ dependencies {
|
|||
implementation project(modulePrefix + 'extension-ima')
|
||||
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.ads.AdsMediaSource;
|
||||
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.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
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.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
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 {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
private final DataSource.Factory manifestDataSourceFactory;
|
||||
private final DataSource.Factory mediaDataSourceFactory;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
|
|
@ -55,21 +50,14 @@ import com.google.android.exoplayer2.util.Util;
|
|||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
manifestDataSourceFactory =
|
||||
dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
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) {
|
||||
// Create a default track selector.
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
|
||||
// Create a player instance.
|
||||
|
|
@ -133,18 +121,13 @@ import com.google.android.exoplayer2.util.Util;
|
|||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
manifestDataSourceFactory)
|
||||
.createMediaSource(uri);
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
||||
.createMediaSource(uri);
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
|
|
@ -70,3 +75,5 @@ dependencies {
|
|||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||
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">
|
||||
|
||||
<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.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
||||
android:exported="false">
|
||||
<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"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
"samples": [
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
@ -330,11 +330,11 @@
|
|||
"samples": [
|
||||
{
|
||||
"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)",
|
||||
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
||||
"drm_scheme": "playready"
|
||||
}
|
||||
]
|
||||
|
|
@ -365,10 +365,6 @@
|
|||
{
|
||||
"name": "Apple AAC media playlist",
|
||||
"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",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Dizzy",
|
||||
"name": "Dizzy (MP4)",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
|
|
@ -391,10 +387,6 @@
|
|||
"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"
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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",
|
||||
"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",
|
||||
"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": [
|
||||
{
|
||||
"name": "Random ABR - 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",
|
||||
"extension": "mpd",
|
||||
"abr_algorithm": "random"
|
||||
"name": "Congo (360 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
},
|
||||
{
|
||||
"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;
|
||||
|
||||
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.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.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
||||
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.CacheDataSource;
|
||||
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_CONTENT_DIRECTORY = "downloads";
|
||||
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;
|
||||
|
||||
|
|
@ -68,16 +55,15 @@ public class DemoApplication extends Application {
|
|||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
||||
public DataSource.Factory buildDataSourceFactory() {
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
||||
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||
}
|
||||
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory(
|
||||
TransferListener<? super DataSource> listener) {
|
||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory() {
|
||||
return new DefaultHttpDataSourceFactory(userAgent);
|
||||
}
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
|
|
@ -98,21 +84,18 @@ public class DemoApplication extends Application {
|
|||
private synchronized void initDownloadManager() {
|
||||
if (downloadManager == null) {
|
||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||
new DownloaderConstructorHelper(
|
||||
getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
|
||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
downloaderConstructorHelper,
|
||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
||||
DOWNLOAD_DESERIALIZERS);
|
||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
|
||||
downloadTracker =
|
||||
new DownloadTracker(
|
||||
/* context= */ this,
|
||||
buildDataSourceFactory(/* listener= */ null),
|
||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
|
||||
DOWNLOAD_DESERIALIZERS);
|
||||
buildDataSourceFactory(),
|
||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
||||
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.DownloadService;
|
||||
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.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
|
|
@ -85,7 +85,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||
Context context,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
File actionFile,
|
||||
DownloadAction.Deserializer[] deserializers) {
|
||||
DownloadAction.Deserializer... deserializers) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.actionFile = new ActionFile(actionFile);
|
||||
|
|
@ -95,7 +95,8 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
||||
actionFileWriteThread.start();
|
||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
||||
loadTrackedActions(deserializers);
|
||||
loadTrackedActions(
|
||||
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
|
|
@ -111,15 +112,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K> List<K> getOfflineStreamKeys(Uri uri) {
|
||||
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||
if (!trackedDownloadStates.containsKey(uri)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
DownloadAction action = trackedDownloadStates.get(uri);
|
||||
if (action instanceof SegmentDownloadAction) {
|
||||
return ((SegmentDownloadAction) action).keys;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
return trackedDownloadStates.get(uri).getKeys();
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
if (!trackKeys.isEmpty()) {
|
||||
builder.setView(dialogView);
|
||||
}
|
||||
builder.create().show();
|
||||
}
|
||||
if (!trackKeys.isEmpty()) {
|
||||
builder.setView(dialogView);
|
||||
}
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -282,6 +279,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||
Toast.makeText(
|
||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Failed to start download", e);
|
||||
}
|
||||
|
||||
@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.MediaCodecUtil.DecoderQueryException;
|
||||
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.ConcatenatingMediaSource;
|
||||
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.AdsMediaSource;
|
||||
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.RepresentationKey;
|
||||
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.RenditionKey;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
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.StreamKey;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
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.PlayerView;
|
||||
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.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
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 ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
private static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
private static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
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.
|
||||
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_AUTO_PLAY = "auto_play";
|
||||
|
||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
static {
|
||||
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
||||
|
|
@ -134,8 +134,9 @@ public class PlayerActivity extends Activity
|
|||
private LinearLayout debugRootView;
|
||||
private TextView debugTextView;
|
||||
|
||||
private DataSource.Factory mediaDataSourceFactory;
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private FrameworkMediaDrm mediaDrm;
|
||||
private MediaSource mediaSource;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
|
|
@ -156,8 +157,12 @@ public class PlayerActivity extends Activity
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
setTheme(R.style.PlayerTheme_Spherical);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||
dataSourceFactory = buildDataSourceFactory();
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
|
|
@ -172,6 +177,21 @@ public class PlayerActivity extends Activity
|
|||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||
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) {
|
||||
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||
|
|
@ -187,6 +207,7 @@ public class PlayerActivity extends Activity
|
|||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
releaseAdsLoader();
|
||||
clearStartPosition();
|
||||
setIntent(intent);
|
||||
}
|
||||
|
|
@ -196,6 +217,9 @@ public class PlayerActivity extends Activity
|
|||
super.onStart();
|
||||
if (Util.SDK_INT > 23) {
|
||||
initializePlayer();
|
||||
if (playerView != null) {
|
||||
playerView.onResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +228,9 @@ public class PlayerActivity extends Activity
|
|||
super.onResume();
|
||||
if (Util.SDK_INT <= 23 || player == null) {
|
||||
initializePlayer();
|
||||
if (playerView != null) {
|
||||
playerView.onResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +238,9 @@ public class PlayerActivity extends Activity
|
|||
public void onPause() {
|
||||
super.onPause();
|
||||
if (Util.SDK_INT <= 23) {
|
||||
if (playerView != null) {
|
||||
playerView.onPause();
|
||||
}
|
||||
releasePlayer();
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +249,9 @@ public class PlayerActivity extends Activity
|
|||
public void onStop() {
|
||||
super.onStop();
|
||||
if (Util.SDK_INT > 23) {
|
||||
if (playerView != null) {
|
||||
playerView.onPause();
|
||||
}
|
||||
releasePlayer();
|
||||
}
|
||||
}
|
||||
|
|
@ -327,7 +360,11 @@ public class PlayerActivity extends Activity
|
|||
finish();
|
||||
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.
|
||||
return;
|
||||
}
|
||||
|
|
@ -368,7 +405,7 @@ public class PlayerActivity extends Activity
|
|||
TrackSelection.Factory trackSelectionFactory;
|
||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||
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)) {
|
||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||
} else {
|
||||
|
|
@ -392,7 +429,8 @@ public class PlayerActivity extends Activity
|
|||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
|
||||
ExoPlayerFactory.newSimpleInstance(
|
||||
/* context= */ this, renderersFactory, trackSelector, drmSessionManager);
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
|
|
@ -441,36 +479,29 @@ public class PlayerActivity extends Activity
|
|||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
return new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setManifestParser(
|
||||
new FilteringManifestParser<>(
|
||||
new DashManifestParser(), (List<RepresentationKey>) getOfflineStreamKeys(uri)))
|
||||
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
return new SsMediaSource.Factory(dataSourceFactory)
|
||||
.setManifestParser(
|
||||
new FilteringManifestParser<>(
|
||||
new SsManifestParser(), (List<StreamKey>) getOfflineStreamKeys(uri)))
|
||||
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setPlaylistParser(
|
||||
new FilteringManifestParser<>(
|
||||
new HlsPlaylistParser(), (List<RenditionKey>) getOfflineStreamKeys(uri)))
|
||||
new FilteringManifestParser<>(new HlsPlaylistParser(), getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<?> getOfflineStreamKeys(Uri uri) {
|
||||
private List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
||||
}
|
||||
|
||||
|
|
@ -478,7 +509,7 @@ public class PlayerActivity extends Activity
|
|||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
HttpDataSource.Factory licenseDataSourceFactory =
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
|
|
@ -487,8 +518,9 @@ public class PlayerActivity extends Activity
|
|||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
return new DefaultDrmSessionManager<>(
|
||||
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
|
||||
releaseMediaDrm();
|
||||
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
|
|
@ -502,6 +534,23 @@ public class PlayerActivity extends Activity
|
|||
mediaSource = 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() {
|
||||
|
|
@ -524,16 +573,9 @@ public class PlayerActivity extends Activity
|
|||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DataSource factory.
|
||||
*
|
||||
* @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 a new DataSource factory. */
|
||||
private DataSource.Factory buildDataSourceFactory() {
|
||||
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
private void updateButtonVisibilities() {
|
||||
|
|
@ -650,7 +683,7 @@ public class PlayerActivity extends Activity
|
|||
return false;
|
||||
}
|
||||
|
||||
private class PlayerEventListener extends Player.DefaultEventListener {
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ import android.os.AsyncTask;
|
|||
import android.os.Bundle;
|
||||
import android.util.JsonReader;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -55,8 +58,11 @@ public class SampleChooserActivity extends Activity
|
|||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
|
||||
private boolean useExtensionRenderers;
|
||||
private DownloadTracker downloadTracker;
|
||||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private MenuItem randomAbrMenuItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -90,13 +96,37 @@ public class SampleChooserActivity extends Activity
|
|||
Arrays.sort(uris);
|
||||
}
|
||||
|
||||
downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker();
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
useExtensionRenderers = application.useExtensionRenderers();
|
||||
downloadTracker = application.getDownloadTracker();
|
||||
SampleListLoader loaderTask = new SampleListLoader();
|
||||
loaderTask.execute(uris);
|
||||
|
||||
// Ping the download service in case it's not running (but should be).
|
||||
startService(
|
||||
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
|
||||
// Start the download service if it should be running but it's not currently.
|
||||
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||
// 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
|
||||
|
|
@ -129,7 +159,13 @@ public class SampleChooserActivity extends Activity
|
|||
public boolean onChildClick(
|
||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -239,10 +275,9 @@ public class SampleChooserActivity extends Activity
|
|||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
boolean drmMultiSession = false;
|
||||
boolean preferExtensionDecoders = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
String abrAlgorithm = null;
|
||||
String sphericalStereoMode = null;
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
|
|
@ -281,11 +316,6 @@ public class SampleChooserActivity extends Activity
|
|||
case "drm_multi_session":
|
||||
drmMultiSession = reader.nextBoolean();
|
||||
break;
|
||||
case "prefer_extension_decoders":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
"Invalid attribute on nested item: prefer_extension_decoders");
|
||||
preferExtensionDecoders = reader.nextBoolean();
|
||||
break;
|
||||
case "playlist":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
playlistSamples = new ArrayList<>();
|
||||
|
|
@ -298,10 +328,10 @@ public class SampleChooserActivity extends Activity
|
|||
case "ad_tag_uri":
|
||||
adTagUri = reader.nextString();
|
||||
break;
|
||||
case "abr_algorithm":
|
||||
case "spherical_stereo_mode":
|
||||
Assertions.checkState(
|
||||
!insidePlaylist, "Invalid attribute on nested item: abr_algorithm");
|
||||
abrAlgorithm = reader.nextString();
|
||||
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||
sphericalStereoMode = reader.nextString();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
|
|
@ -315,11 +345,15 @@ public class SampleChooserActivity extends Activity
|
|||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(
|
||||
new UriSample[playlistSamples.size()]);
|
||||
return new PlaylistSample(
|
||||
sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray);
|
||||
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
|
||||
} else {
|
||||
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 {
|
||||
public final String name;
|
||||
public final boolean preferExtensionDecoders;
|
||||
public final String abrAlgorithm;
|
||||
public final DrmInfo drmInfo;
|
||||
|
||||
public Sample(
|
||||
String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) {
|
||||
public Sample(String name, DrmInfo drmInfo) {
|
||||
this.name = name;
|
||||
this.preferExtensionDecoders = preferExtensionDecoders;
|
||||
this.abrAlgorithm = abrAlgorithm;
|
||||
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.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
|
||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
|
|
@ -506,27 +536,30 @@ public class SampleChooserActivity extends Activity
|
|||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final String adTagUri;
|
||||
public final String sphericalStereoMode;
|
||||
|
||||
public UriSample(
|
||||
String name,
|
||||
boolean preferExtensionDecoders,
|
||||
String abrAlgorithm,
|
||||
DrmInfo drmInfo,
|
||||
Uri uri,
|
||||
String extension,
|
||||
String adTagUri) {
|
||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||
String adTagUri,
|
||||
String sphericalStereoMode) {
|
||||
super(name, drmInfo);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent buildIntent(Context context) {
|
||||
return super.buildIntent(context)
|
||||
public Intent buildIntent(
|
||||
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||
.setData(uri)
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
|
||||
.setAction(PlayerActivity.ACTION_VIEW);
|
||||
}
|
||||
|
||||
|
|
@ -538,23 +571,22 @@ public class SampleChooserActivity extends Activity
|
|||
|
||||
public PlaylistSample(
|
||||
String name,
|
||||
boolean preferExtensionDecoders,
|
||||
String abrAlgorithm,
|
||||
DrmInfo drmInfo,
|
||||
UriSample... children) {
|
||||
super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
|
||||
super(name, drmInfo);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent buildIntent(Context context) {
|
||||
public Intent buildIntent(
|
||||
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
|
||||
String[] uris = new String[children.length];
|
||||
String[] extensions = new String[children.length];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
uris[i] = children[i].uri.toString();
|
||||
extensions[i] = children[i].extension;
|
||||
}
|
||||
return super.buildIntent(context)
|
||||
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
|
||||
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
|
||||
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
|
||||
.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="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||
|
||||
<string name="error_generic">Playback failed</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_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="prefer_extension_decoders">Prefer extension decoders</string>
|
||||
|
||||
<string name="random_abr">Enable random ABR</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -20,4 +20,8 @@
|
|||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
@ -26,17 +31,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// 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:
|
||||
// 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
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.0.1'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
|
|
@ -44,8 +39,19 @@ dependencies {
|
|||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
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 {
|
||||
javadocTitle = 'Cast extension'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,6 +283,11 @@ public final class CastPlayer implements Player {
|
|||
|
||||
// Player implementation.
|
||||
|
||||
@Override
|
||||
public AudioComponent getAudioComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoComponent getVideoComponent() {
|
||||
return null;
|
||||
|
|
@ -526,6 +531,15 @@ public final class CastPlayer implements Player {
|
|||
: 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
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
return !currentTimeline.isEmpty()
|
||||
|
|
@ -563,6 +577,11 @@ public final class CastPlayer implements Player {
|
|||
return getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentBufferedPosition() {
|
||||
return getBufferedPosition();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
public void updateInternalState() {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ import java.util.Map;
|
|||
/* package */ final class CastTimeline extends Timeline {
|
||||
|
||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||
new CastTimeline(
|
||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
||||
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
|
||||
|
||||
private final SparseIntArray idsToIndex;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getUidOfPeriod(int periodIndex) {
|
||||
return ids[periodIndex];
|
||||
}
|
||||
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack;
|
|||
* @return The equivalent {@link Format}.
|
||||
*/
|
||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
|
||||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
||||
return Format.createContainerFormat(
|
||||
mediaTrack.getContentId(),
|
||||
/* label= */ null,
|
||||
mediaTrack.getContentType(),
|
||||
/* sampleMimeType= */ null,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* selectionFlags= */ 0,
|
||||
mediaTrack.getLanguage());
|
||||
}
|
||||
|
||||
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
|
||||
[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
|
||||
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:
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
|
||||
directory
|
||||
1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
|
||||
`cronet_impl_native_java.jar` and the `libs` directory
|
||||
1. Copy the three jar files into the `libs` directory of this extension
|
||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||
directory of this extension
|
||||
1. In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
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.
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:extension-cronet:2.X.X'
|
||||
```
|
||||
|
||||
where `2.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[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 ##
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,10 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDirs = ['jniLibs']
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
|
@ -34,9 +30,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api files('libs/cronet_api.jar')
|
||||
implementation files('libs/cronet_impl_common_java.jar')
|
||||
implementation files('libs/cronet_impl_native_java.jar')
|
||||
api 'org.chromium.net:cronet-embedded:66.3359.158'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
|
|
@ -47,3 +41,9 @@ ext {
|
|||
javadocTitle = 'Cronet extension'
|
||||
}
|
||||
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 com.google.android.exoplayer2.C;
|
||||
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.DataSpec;
|
||||
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.Clock;
|
||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
|
|
@ -32,6 +32,7 @@ import java.io.IOException;
|
|||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.
|
||||
*
|
||||
* <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}.
|
||||
|
|
@ -95,6 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
*/
|
||||
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 CONTENT_TYPE = "Content-Type";
|
||||
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 Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener<? super CronetDataSource> listener;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
|
|
@ -143,57 +146,73 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
||||
* to make sure response handling is a fast operation when using a direct executor.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* #open(DataSpec)}.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null, false);
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine, Executor executor, Predicate<String> contentTypePredicate) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||
false,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
||||
* to make sure response handling is a fast operation when using a direct executor.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* #open(DataSpec)}.
|
||||
* @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 defaultRequestProperties The default request properties to be used.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false);
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
||||
* to make sure response handling is a fast operation when using a direct executor.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* #open(DataSpec)}.
|
||||
* @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.
|
||||
|
|
@ -201,23 +220,42 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||
* the redirect url in the "Cookie" header.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties,
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties,
|
||||
boolean handleSetCookieRequests) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
contentTypePredicate,
|
||||
connectTimeoutMs,
|
||||
readTimeoutMs,
|
||||
resetTimeoutOnRedirects,
|
||||
Clock.DEFAULT,
|
||||
defaultRequestProperties,
|
||||
handleSetCookieRequests);
|
||||
}
|
||||
|
||||
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
/* package */ CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
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.executor = Assertions.checkNotNull(executor);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.listener = listener;
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
|
|
@ -247,7 +285,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return responseInfo == null ? null : responseInfo.getAllHeaders();
|
||||
return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -270,6 +308,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
}
|
||||
currentUrlRequest.start();
|
||||
|
||||
transferInitializing(dataSpec);
|
||||
try {
|
||||
boolean connectionOpened = blockUntilConnectTimeout();
|
||||
if (exception != null) {
|
||||
|
|
@ -323,9 +362,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
}
|
||||
|
||||
opened = true;
|
||||
if (listener != null) {
|
||||
listener.onTransferStart(this, dataSpec);
|
||||
}
|
||||
transferStarted(dataSpec);
|
||||
|
||||
return bytesRemaining;
|
||||
}
|
||||
|
|
@ -391,9 +428,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onBytesTransferred(this, bytesRead);
|
||||
}
|
||||
bytesTransferred(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
|
|
@ -412,107 +447,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
finished = false;
|
||||
if (opened) {
|
||||
opened = false;
|
||||
if (listener != null) {
|
||||
listener.onTransferEnd(this);
|
||||
}
|
||||
transferEnded();
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
||||
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
|
||||
dataSpec.uri.toString(), this, executor).allowDirectExecutor();
|
||||
UrlRequest.Builder requestBuilder =
|
||||
cronetEngine
|
||||
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
|
||||
.allowDirectExecutor();
|
||||
// Set the headers.
|
||||
boolean isContentTypeHeaderSet = false;
|
||||
if (defaultRequestProperties != null) {
|
||||
|
|
@ -528,8 +473,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
|
||||
throw new IOException("POST request with non-empty body must set Content-Type");
|
||||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||
}
|
||||
// Set the Range header.
|
||||
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");
|
||||
// }
|
||||
// Set the method and (if non-empty) the body.
|
||||
if (dataSpec.postBody != null) {
|
||||
requestBuilder.setHttpMethod("POST");
|
||||
if (dataSpec.postBody.length != 0) {
|
||||
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
|
||||
executor);
|
||||
}
|
||||
requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
|
||||
if (dataSpec.httpBody != null) {
|
||||
requestBuilder.setUploadDataProvider(
|
||||
new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
|
||||
}
|
||||
return requestBuilder;
|
||||
}
|
||||
|
|
@ -655,4 +598,91 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
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;
|
||||
|
||||
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.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
|
|
@ -46,7 +46,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
private final CronetEngineWrapper cronetEngineWrapper;
|
||||
private final Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener<? super DataSource> transferListener;
|
||||
private final @Nullable TransferListener transferListener;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
|
|
@ -54,26 +54,176 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* <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.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @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,
|
||||
TransferListener<? super DataSource> transferListener,
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
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) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
||||
|
|
@ -81,25 +231,28 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* <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}.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor, Predicate<String> contentTypePredicate,
|
||||
TransferListener<? super DataSource> transferListener, String userAgent) {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
||||
|
|
@ -108,25 +261,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* <p>
|
||||
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
||||
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <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}.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @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,
|
||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||
int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
String userAgent) {
|
||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
||||
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
||||
|
|
@ -135,26 +293,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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}.
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||
* CronetDataSource#open}.
|
||||
* @param transferListener An optional listener.
|
||||
* @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.
|
||||
* @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,
|
||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||
int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable TransferListener transferListener,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
HttpDataSource.Factory fallbackFactory) {
|
||||
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||
this.executor = executor;
|
||||
|
|
@ -173,8 +335,19 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
if (cronetEngine == null) {
|
||||
return fallbackFactory.createDataSource();
|
||||
}
|
||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
|
||||
CronetDataSource dataSource =
|
||||
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.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
|
@ -81,13 +80,14 @@ public final class CronetDataSourceTest {
|
|||
|
||||
private DataSpec testDataSpec;
|
||||
private DataSpec testPostDataSpec;
|
||||
private DataSpec testHeadDataSpec;
|
||||
private Map<String, String> testResponseHeader;
|
||||
private UrlResponseInfo testUrlResponseInfo;
|
||||
|
||||
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
||||
@Mock private UrlRequest mockUrlRequest;
|
||||
@Mock private Predicate<String> mockContentTypePredicate;
|
||||
@Mock private TransferListener<CronetDataSource> mockTransferListener;
|
||||
@Mock private TransferListener mockTransferListener;
|
||||
@Mock private Executor mockExecutor;
|
||||
@Mock private NetworkException mockNetworkException;
|
||||
@Mock private CronetEngine mockCronetEngine;
|
||||
|
|
@ -99,18 +99,17 @@ public final class CronetDataSourceTest {
|
|||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
false));
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
false);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||
when(mockCronetEngine.newUrlRequestBuilder(
|
||||
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);
|
||||
testPostDataSpec =
|
||||
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.put("Content-Type", TEST_CONTENT_TYPE);
|
||||
// This value can be anything since the DataSpec is unset.
|
||||
|
|
@ -172,9 +174,10 @@ public final class CronetDataSourceTest {
|
|||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
// Invoke the callback for the previous request.
|
||||
dataSourceUnderTest.onFailed(
|
||||
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||
mockUrlRequest2, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -213,7 +216,8 @@ public final class CronetDataSourceTest {
|
|||
public void testRequestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
verify(mockTransferListener)
|
||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -225,7 +229,8 @@ public final class CronetDataSourceTest {
|
|||
mockResponseStartSuccess();
|
||||
|
||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
verify(mockTransferListener)
|
||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -239,7 +244,8 @@ public final class CronetDataSourceTest {
|
|||
// Check for connection not automatically closed.
|
||||
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
|
||||
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.
|
||||
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
|
||||
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();
|
||||
// Check for connection not automatically closed.
|
||||
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);
|
||||
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
|
||||
verify(mockTransferListener)
|
||||
.onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);
|
||||
}
|
||||
|
||||
@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
|
||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
|
@ -346,7 +364,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// Should have only called read on cronet once.
|
||||
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
|
||||
|
|
@ -386,7 +405,8 @@ public final class CronetDataSourceTest {
|
|||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -402,7 +422,8 @@ public final class CronetDataSourceTest {
|
|||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -418,7 +439,8 @@ public final class CronetDataSourceTest {
|
|||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -433,7 +455,8 @@ public final class CronetDataSourceTest {
|
|||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
|
||||
assertThat(bytesRead).isEqualTo(8);
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -447,7 +470,8 @@ public final class CronetDataSourceTest {
|
|||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
|
||||
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
|
||||
assertThat(bytesRead).isEqualTo(16);
|
||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||
verify(mockTransferListener)
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -464,7 +488,8 @@ public final class CronetDataSourceTest {
|
|||
assertThat(bytesRead).isEqualTo(8);
|
||||
|
||||
dataSourceUnderTest.close();
|
||||
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
|
||||
verify(mockTransferListener)
|
||||
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
|
||||
try {
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
|
|
@ -505,9 +530,12 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// Should have only called read on cronet once.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 8);
|
||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 6);
|
||||
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 2);
|
||||
verify(mockTransferListener, times(1))
|
||||
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||
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.
|
||||
// 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]);
|
||||
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||
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.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
// Check for connection not automatically closed.
|
||||
|
|
@ -559,7 +588,8 @@ public final class CronetDataSourceTest {
|
|||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
verify(mockTransferListener, never())
|
||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -597,11 +627,12 @@ public final class CronetDataSourceTest {
|
|||
thread.interrupt();
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
verify(mockTransferListener, never())
|
||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectResponseBeforeTimeout() throws InterruptedException {
|
||||
public void testConnectResponseBeforeTimeout() throws Exception {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch openLatch = new CountDownLatch(1);
|
||||
|
|
@ -625,12 +656,12 @@ public final class CronetDataSourceTest {
|
|||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(openLatch);
|
||||
// The response arrives just in time.
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
openLatch.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
|
||||
public void testRedirectIncreasesConnectionTimeout() throws Exception {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
|
|
@ -659,7 +690,7 @@ public final class CronetDataSourceTest {
|
|||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
||||
|
||||
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.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
||||
|
||||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
||||
|
|
@ -678,7 +709,8 @@ public final class CronetDataSourceTest {
|
|||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
verify(mockTransferListener, never())
|
||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
assertThat(openExceptions.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
|
|
@ -700,18 +732,17 @@ public final class CronetDataSourceTest {
|
|||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
|
@ -732,18 +763,17 @@ public final class CronetDataSourceTest {
|
|||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
|
@ -772,18 +802,17 @@ public final class CronetDataSourceTest {
|
|||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true);
|
||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
|
|
@ -800,7 +829,7 @@ public final class CronetDataSourceTest {
|
|||
// the subsequent open() call succeeds.
|
||||
doThrow(new NullPointerException())
|
||||
.when(mockTransferListener)
|
||||
.onTransferEnd(dataSourceUnderTest);
|
||||
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
try {
|
||||
dataSourceUnderTest.close();
|
||||
|
|
@ -889,7 +918,8 @@ public final class CronetDataSourceTest {
|
|||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||
mockUrlRequest, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -902,7 +932,7 @@ public final class CronetDataSourceTest {
|
|||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(307), // statusCode
|
||||
"http://redirect.location.com");
|
||||
|
|
@ -920,12 +950,13 @@ public final class CronetDataSourceTest {
|
|||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (!redirectCalled) {
|
||||
redirectCalled = true;
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||
"http://example.com/video/redirect");
|
||||
} else {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||
mockUrlRequest, testUrlResponseInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -939,7 +970,8 @@ public final class CronetDataSourceTest {
|
|||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(
|
||||
mockUrlRequest, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -952,7 +984,7 @@ public final class CronetDataSourceTest {
|
|||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
|
|
@ -970,14 +1002,15 @@ public final class CronetDataSourceTest {
|
|||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (positionAndRemaining[1] == 0) {
|
||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||
dataSourceUnderTest.urlRequestCallback.onSucceeded(
|
||||
mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
dataSourceUnderTest.onReadCompleted(
|
||||
dataSourceUnderTest.urlRequestCallback.onReadCompleted(
|
||||
mockUrlRequest, testUrlResponseInfo, inputBuffer);
|
||||
}
|
||||
return null;
|
||||
|
|
@ -992,7 +1025,7 @@ public final class CronetDataSourceTest {
|
|||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
dataSourceUnderTest.urlRequestCallback.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ COMMON_OPTIONS="\
|
|||
--enable-decoder=flac \
|
||||
" && \
|
||||
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 \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
@ -32,6 +37,8 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
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.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Decodes and renders audio using FFmpeg.
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
|
||||
* insufficient.
|
||||
*/
|
||||
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 final boolean enableFloatOutput;
|
||||
|
||||
private FfmpegDecoder decoder;
|
||||
private @MonotonicNonNull FfmpegDecoder decoder;
|
||||
|
||||
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 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) {
|
||||
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
|
||||
* adjustment.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink, boolean enableFloatOutput) {
|
||||
public FfmpegAudioRenderer(
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink,
|
||||
boolean enableFloatOutput) {
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
|
|
@ -86,10 +94,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
@Override
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
String sampleMimeType = format.sampleMimeType;
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
|
||||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
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;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
|
|
@ -106,18 +115,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
@Override
|
||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
decoder =
|
||||
new FfmpegDecoder(
|
||||
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getOutputFormat() {
|
||||
Assertions.checkNotNull(decoder);
|
||||
int channelCount = decoder.getChannelCount();
|
||||
int sampleRate = decoder.getSampleRate();
|
||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
||||
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
|
||||
return Format.createAudioSampleFormat(
|
||||
/* 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) {
|
||||
|
|
@ -125,6 +149,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
||||
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
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.SimpleDecoder;
|
||||
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.ParsableByteArray;
|
||||
import java.nio.ByteBuffer;
|
||||
|
|
@ -30,13 +33,12 @@ import java.util.List;
|
|||
/* package */ final class FfmpegDecoder extends
|
||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||
|
||||
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
|
||||
// Space for 64 ms of 48 KhZ 8 channel 32-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 = 65536;
|
||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||
|
||||
private final String codecName;
|
||||
private final byte[] extraData;
|
||||
private final @Nullable byte[] extraData;
|
||||
private final @C.Encoding int encoding;
|
||||
private final int outputBufferSize;
|
||||
|
||||
|
|
@ -45,18 +47,26 @@ import java.util.List;
|
|||
private volatile int channelCount;
|
||||
private volatile int sampleRate;
|
||||
|
||||
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||
String mimeType, List<byte[]> initializationData, boolean outputFloat)
|
||||
public FfmpegDecoder(
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers,
|
||||
int initialInputBufferSize,
|
||||
Format format,
|
||||
boolean outputFloat)
|
||||
throws FfmpegDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||
}
|
||||
codecName = FfmpegLibrary.getCodecName(mimeType);
|
||||
extraData = getExtraData(mimeType, initializationData);
|
||||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
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;
|
||||
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) {
|
||||
throw new FfmpegDecoderException("Initialization failed.");
|
||||
}
|
||||
|
|
@ -84,7 +94,7 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoderException decode(
|
||||
protected @Nullable FfmpegDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
nativeContext = ffmpegReset(nativeContext, extraData);
|
||||
|
|
@ -103,6 +113,7 @@ import java.util.List;
|
|||
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||
sampleRate = ffmpegGetSampleRate(nativeContext);
|
||||
if (sampleRate == 0 && "alac".equals(codecName)) {
|
||||
Assertions.checkNotNull(extraData);
|
||||
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
||||
// See https://trac.ffmpeg.org/ticket/6096
|
||||
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
|
||||
* not required.
|
||||
*/
|
||||
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
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,
|
||||
ByteBuffer outputData, int outputSize);
|
||||
private native int ffmpegGetChannelCount(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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
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.util.LibraryLoader;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -51,10 +53,8 @@ public final class FfmpegLibrary {
|
|||
return LOADER.isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the underlying library if available, or null otherwise.
|
||||
*/
|
||||
public static String getVersion() {
|
||||
/** Returns the version of the underlying library if available, or null otherwise. */
|
||||
public static @Nullable String getVersion() {
|
||||
return isAvailable() ? ffmpegGetVersion() : null;
|
||||
}
|
||||
|
||||
|
|
@ -62,19 +62,21 @@ public final class FfmpegLibrary {
|
|||
* Returns whether the underlying library supports the specified MIME type.
|
||||
*
|
||||
* @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()) {
|
||||
return false;
|
||||
}
|
||||
String codecName = getCodecName(mimeType);
|
||||
String codecName = getCodecName(mimeType, encoding);
|
||||
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) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
return "aac";
|
||||
|
|
@ -85,6 +87,7 @@ public final class FfmpegLibrary {
|
|||
case MimeTypes.AUDIO_AC3:
|
||||
return "ac3";
|
||||
case MimeTypes.AUDIO_E_AC3:
|
||||
case MimeTypes.AUDIO_E_AC3_JOC:
|
||||
return "eac3";
|
||||
case MimeTypes.AUDIO_TRUEHD:
|
||||
return "truehd";
|
||||
|
|
@ -103,6 +106,14 @@ public final class FfmpegLibrary {
|
|||
return "flac";
|
||||
case MimeTypes.AUDIO_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:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ extern "C" {
|
|||
#endif
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavresample/avresample.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/error.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.
|
||||
* Returns the created context.
|
||||
*/
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData, jboolean outputFloat);
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||
jboolean outputFloat, jint rawSampleRate,
|
||||
jint rawChannelCount);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
jboolean outputFloat) {
|
||||
jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
|
||||
AVCodec *codec = getCodecByName(env, codecName);
|
||||
if (!codec) {
|
||||
LOGE("Codec not found.");
|
||||
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,
|
||||
|
|
@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
|||
LOGE("Unexpected error finding codec %d.", codecId);
|
||||
return 0L;
|
||||
}
|
||||
return (jlong) createContext(env, codec, extraData,
|
||||
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
||||
jboolean outputFloat =
|
||||
(jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
||||
return (jlong)createContext(env, codec, extraData, outputFloat,
|
||||
/* rawSampleRate= */ -1,
|
||||
/* rawChannelCount= */ -1);
|
||||
}
|
||||
|
||||
avcodec_flush_buffers(context);
|
||||
|
|
@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
|
|||
return codec;
|
||||
}
|
||||
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData, jboolean outputFloat) {
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||
jboolean outputFloat, jint rawSampleRate,
|
||||
jint rawChannelCount) {
|
||||
AVCodecContext *context = avcodec_alloc_context3(codec);
|
||||
if (!context) {
|
||||
LOGE("Failed to allocate context.");
|
||||
|
|
@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
|||
}
|
||||
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);
|
||||
if (result < 0) {
|
||||
logError("avcodec_open2", result);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
|||
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||
|
||||
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
|
||||
implements Runnable {
|
||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
|
|||
|
|
@ -15,15 +15,11 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
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.SeekPoint;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
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
|
||||
* 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 long firstFramePosition;
|
||||
private final long inputLength;
|
||||
private final long approxBytesPerFrame;
|
||||
|
||||
private @Nullable SeekOperationParams pendingSeekOperationParams;
|
||||
|
||||
public FlacBinarySearchSeeker(
|
||||
FlacStreamInfo streamInfo,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
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.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. */
|
||||
public SeekMap getSeekMap() {
|
||||
return seekMap;
|
||||
@Override
|
||||
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
|
||||
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. */
|
||||
public void setSeekTargetUs(long timeUs) {
|
||||
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
|
||||
return;
|
||||
private static final class FlacTimestampSeeker implements TimestampSeeker {
|
||||
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
|
||||
this.decoderJni = decoderJni;
|
||||
}
|
||||
|
||||
pendingSeekOperationParams =
|
||||
new SeekOperationParams(
|
||||
timeUs,
|
||||
streamInfo.getSampleIndex(timeUs),
|
||||
/* floorSample= */ 0,
|
||||
/* ceilingSample= */ streamInfo.totalSamples,
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimestampSearchResult searchForTimestamp(
|
||||
ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
|
||||
throws IOException, InterruptedException {
|
||||
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
||||
long searchPosition = input.getPosition();
|
||||
decoderJni.reset(searchPosition);
|
||||
try {
|
||||
decoderJni.decodeSampleWithBacktrackPosition(
|
||||
|
|
@ -145,11 +81,10 @@ import java.nio.ByteBuffer;
|
|||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||
// 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.
|
||||
pendingSeekOperationParams = null;
|
||||
throw new IOException("Cannot read frame at position " + searchPosition, e);
|
||||
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||
}
|
||||
if (outputBuffer.limit() == 0) {
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||
}
|
||||
|
||||
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
||||
|
|
@ -157,184 +92,35 @@ import java.nio.ByteBuffer;
|
|||
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
||||
|
||||
boolean targetSampleInLastFrame =
|
||||
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
|
||||
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
|
||||
lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;
|
||||
|
||||
if (targetSampleInLastFrame) {
|
||||
pendingSeekOperationParams = null;
|
||||
return Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
|
||||
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
|
||||
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
||||
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
||||
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
||||
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
||||
return TimestampSearchResult.underestimatedResult(
|
||||
nextFrameSampleIndex, nextFrameSamplePosition);
|
||||
} 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}.
|
||||
*
|
||||
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
|
||||
* [floorPosition, ceilingPosition).
|
||||
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
|
||||
* the timestamp for a stream seek time position.
|
||||
*/
|
||||
private static final class SeekOperationParams {
|
||||
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 static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
||||
private final FlacStreamInfo streamInfo;
|
||||
private final long firstFramePosition;
|
||||
private final long inputLength;
|
||||
private final long approxBytesPerFrame;
|
||||
private final long durationUs;
|
||||
|
||||
private FlacBinarySearchSeekMap(
|
||||
FlacStreamInfo streamInfo,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
long durationUs,
|
||||
long approxBytesPerFrame) {
|
||||
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
this.inputLength = inputLength;
|
||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@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;
|
||||
public long timeUsToTargetTime(long timeUs) {
|
||||
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
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.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
|
|
@ -37,11 +38,17 @@ import java.util.List;
|
|||
*
|
||||
* @param numInputBuffers The number of input 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
|
||||
* which is the flac file header.
|
||||
* which is the flac file header.
|
||||
* @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 {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
if (initializationData.size() != 1) {
|
||||
|
|
@ -60,7 +67,9 @@ import java.util.List;
|
|||
throw new FlacDecoderException("Metadata decoding failed");
|
||||
}
|
||||
|
||||
setInitialInputBufferSize(streamInfo.maxFrameSize);
|
||||
int initialInputBufferSize =
|
||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
|
||||
setInitialInputBufferSize(initialInputBufferSize);
|
||||
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
|
|||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -46,24 +47,14 @@ import java.util.Arrays;
|
|||
*/
|
||||
public final class FlacExtractor implements Extractor {
|
||||
|
||||
/**
|
||||
* Factory that returns one extractor which is a {@link FlacExtractor}.
|
||||
*/
|
||||
public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
|
||||
|
||||
@Override
|
||||
public Extractor[] createExtractors() {
|
||||
return new Extractor[] {new FlacExtractor()};
|
||||
}
|
||||
|
||||
};
|
||||
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
|
||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
|
||||
|
||||
/** Flags controlling the behavior of the extractor. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = {FLAG_DISABLE_ID3_METADATA}
|
||||
)
|
||||
flag = true,
|
||||
value = {FLAG_DISABLE_ID3_METADATA})
|
||||
public @interface Flags {}
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +79,7 @@ public final class FlacExtractor implements Extractor {
|
|||
|
||||
private ParsableByteArray outputBuffer;
|
||||
private ByteBuffer outputByteBuffer;
|
||||
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
|
||||
private FlacStreamInfo streamInfo;
|
||||
|
||||
private Metadata id3Metadata;
|
||||
|
|
@ -140,7 +132,7 @@ public final class FlacExtractor implements Extractor {
|
|||
decoderJni.setData(input);
|
||||
readPastStreamInfo(input);
|
||||
|
||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
|
||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
|
||||
return handlePendingSeek(input, seekPosition);
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +216,7 @@ public final class FlacExtractor implements Extractor {
|
|||
outputFormat(streamInfo);
|
||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
||||
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
|
||||
}
|
||||
|
||||
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
||||
|
|
@ -286,9 +279,10 @@ public final class FlacExtractor implements Extractor {
|
|||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws InterruptedException, IOException {
|
||||
int seekResult =
|
||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
|
||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
|
||||
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
|
||||
}
|
||||
return seekResult;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
@Override
|
||||
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
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
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
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.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.vr.sdk.audio.GvrAudioSurround;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
|
@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||
@Override
|
||||
public void queueInput(ByteBuffer input) {
|
||||
int position = input.position();
|
||||
Assertions.checkNotNull(gvrAudioSurround);
|
||||
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
|
||||
input.position(position + readBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEndOfStream() {
|
||||
Assertions.checkNotNull(gvrAudioSurround);
|
||||
inputEnded = true;
|
||||
gvrAudioSurround.triggerProcessing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
Assertions.checkNotNull(gvrAudioSurround);
|
||||
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
||||
buffer.position(0).limit(writtenBytes);
|
||||
return buffer;
|
||||
|
|
@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
Assertions.checkNotNull(gvrAudioSurround);
|
||||
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
@ -26,19 +31,25 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// 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'
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4'
|
||||
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 {
|
||||
javadocTitle = 'IMA extension'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import android.support.annotation.IntDef;
|
|||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||
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.AdsRenderingSettings;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
|
||||
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
|
||||
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
|
||||
|
|
@ -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.AdsLoader;
|
||||
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.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -62,15 +63,20 @@ import java.lang.annotation.Retention;
|
|||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Loads ads using the IMA SDK. All methods are called on the main thread.
|
||||
*/
|
||||
public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader,
|
||||
VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
|
||||
/** Loads ads using the IMA SDK. All methods are called on the main thread. */
|
||||
public final class ImaAdsLoader
|
||||
implements Player.EventListener,
|
||||
AdsLoader,
|
||||
VideoAdPlayer,
|
||||
ContentProgressProvider,
|
||||
AdErrorListener,
|
||||
AdsLoadedListener,
|
||||
AdEventListener {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
||||
|
|
@ -85,6 +91,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
private @Nullable AdEventListener adEventListener;
|
||||
private int vastLoadTimeoutMs;
|
||||
private int mediaLoadTimeoutMs;
|
||||
private ImaFactory imaFactory;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
||||
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
||||
imaFactory = new DefaultImaFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,6 +157,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
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.
|
||||
*
|
||||
|
|
@ -165,7 +179,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
null,
|
||||
vastLoadTimeoutMs,
|
||||
mediaLoadTimeoutMs,
|
||||
adEventListener);
|
||||
adEventListener,
|
||||
imaFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,7 +198,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
adsResponse,
|
||||
vastLoadTimeoutMs,
|
||||
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. */
|
||||
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;
|
||||
|
||||
/** 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 mediaLoadTimeoutMs;
|
||||
private final @Nullable AdEventListener adEventListener;
|
||||
private final ImaFactory imaFactory;
|
||||
private final Timeline.Period period;
|
||||
private final List<VideoAdPlayerCallback> adCallbacks;
|
||||
private final ImaSdkFactory imaSdkFactory;
|
||||
private final AdDisplayContainer adDisplayContainer;
|
||||
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 EventListener eventListener;
|
||||
private Player player;
|
||||
private ViewGroup adUiViewGroup;
|
||||
private VideoProgressUpdate lastContentProgress;
|
||||
private VideoProgressUpdate lastAdProgress;
|
||||
private int lastVolumePercentage;
|
||||
|
||||
private AdsManager adsManager;
|
||||
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. */
|
||||
private int expectedAdGroupIndex;
|
||||
/**
|
||||
* The index of the current ad group that IMA is loading.
|
||||
*/
|
||||
/** The index of the current ad group that IMA is loading. */
|
||||
private int adGroupIndex;
|
||||
/**
|
||||
* Whether IMA has sent an ad event to pause content since the last resume content event.
|
||||
*/
|
||||
/** Whether IMA has sent an ad event to pause content since the last resume content event. */
|
||||
private boolean imaPausedContent;
|
||||
/** The current ad playback state. */
|
||||
private @ImaAdState int imaAdState;
|
||||
|
|
@ -285,9 +289,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
|
||||
// Fields tracking the player/loader state.
|
||||
|
||||
/**
|
||||
* Whether the player is playing an ad.
|
||||
*/
|
||||
/** Whether the player is playing an ad. */
|
||||
private boolean playingAd;
|
||||
/**
|
||||
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
|
||||
|
|
@ -310,13 +312,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
* content progress should increase. {@link C#TIME_UNSET} otherwise.
|
||||
*/
|
||||
private long fakeContentProgressOffsetMs;
|
||||
/**
|
||||
* Stores the pending content position when a seek operation was intercepted to play an ad.
|
||||
*/
|
||||
/** Stores the pending content position when a seek operation was intercepted to play an ad. */
|
||||
private long pendingContentPositionMs;
|
||||
/**
|
||||
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
|
||||
*/
|
||||
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
|
||||
private boolean sentPendingContentPositionMs;
|
||||
|
||||
/**
|
||||
|
|
@ -337,7 +335,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
/* adsResponse= */ null,
|
||||
/* vastLoadTimeoutMs= */ 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,
|
||||
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
|
||||
/* adEventListener= */ null);
|
||||
/* adEventListener= */ null,
|
||||
/* imaFactory= */ new DefaultImaFactory());
|
||||
}
|
||||
|
||||
private ImaAdsLoader(
|
||||
|
|
@ -370,26 +370,30 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
@Nullable String adsResponse,
|
||||
int vastLoadTimeoutMs,
|
||||
int mediaLoadTimeoutMs,
|
||||
@Nullable AdEventListener adEventListener) {
|
||||
@Nullable AdEventListener adEventListener,
|
||||
ImaFactory imaFactory) {
|
||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||
this.adTagUri = adTagUri;
|
||||
this.adsResponse = adsResponse;
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
|
||||
this.adEventListener = adEventListener;
|
||||
period = new Timeline.Period();
|
||||
adCallbacks = new ArrayList<>(1);
|
||||
imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
|
||||
adDisplayContainer.setPlayer(this);
|
||||
this.imaFactory = imaFactory;
|
||||
if (imaSdkSettings == null) {
|
||||
imaSdkSettings = imaSdkFactory.createImaSdkSettings();
|
||||
imaSdkSettings = imaFactory.createImaSdkSettings();
|
||||
if (DEBUG) {
|
||||
imaSdkSettings.setDebugMode(true);
|
||||
}
|
||||
}
|
||||
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
||||
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
||||
adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
|
||||
adsLoader.addAdErrorListener(this);
|
||||
adsLoader.addAdsLoadedListener(this);
|
||||
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
|
||||
period = new Timeline.Period();
|
||||
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
||||
adDisplayContainer = imaFactory.createAdDisplayContainer();
|
||||
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
|
||||
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
|
||||
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
fakeContentProgressOffsetMs = C.TIME_UNSET;
|
||||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
|
|
@ -405,6 +409,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
return adsLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
|
||||
* ImaSdkFactory#createCompanionAdSlot()}.
|
||||
*
|
||||
* @param companionSlots Slots for displaying companion ads.
|
||||
* @see AdDisplayContainer#setCompanionSlots(Collection)
|
||||
*/
|
||||
public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {
|
||||
adDisplayContainer.setCompanionSlots(companionSlots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests ads, if they have not already been requested. Must be called on the main thread.
|
||||
*
|
||||
|
|
@ -421,7 +436,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
}
|
||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
pendingAdRequestContext = new Object();
|
||||
AdsRequest request = imaSdkFactory.createAdsRequest();
|
||||
AdsRequest request = imaFactory.createAdsRequest();
|
||||
if (adTagUri != null) {
|
||||
request.setAdTagUrl(adTagUri.toString());
|
||||
} else /* adsResponse != null */ {
|
||||
|
|
@ -447,9 +462,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
} else if (contentType == C.TYPE_HLS) {
|
||||
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
||||
} else if (contentType == C.TYPE_OTHER) {
|
||||
supportedMimeTypes.addAll(Arrays.asList(
|
||||
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
|
||||
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
|
||||
supportedMimeTypes.addAll(
|
||||
Arrays.asList(
|
||||
MimeTypes.VIDEO_MP4,
|
||||
MimeTypes.VIDEO_WEBM,
|
||||
MimeTypes.VIDEO_H263,
|
||||
MimeTypes.AUDIO_MP4,
|
||||
MimeTypes.AUDIO_MPEG));
|
||||
} else if (contentType == C.TYPE_SS) {
|
||||
// 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) {
|
||||
this.player = player;
|
||||
this.eventListener = eventListener;
|
||||
this.adUiViewGroup = adUiViewGroup;
|
||||
lastVolumePercentage = 0;
|
||||
lastAdProgress = null;
|
||||
lastContentProgress = null;
|
||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
|
|
@ -490,12 +509,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
|
||||
adsManager.pause();
|
||||
}
|
||||
lastVolumePercentage = getVolume();
|
||||
lastAdProgress = getAdProgress();
|
||||
lastContentProgress = getContentProgress();
|
||||
player.removeListener(this);
|
||||
player = null;
|
||||
eventListener = null;
|
||||
adUiViewGroup = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -505,6 +524,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
adsManager.destroy();
|
||||
adsManager = null;
|
||||
}
|
||||
imaPausedContent = false;
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
pendingAdLoadError = null;
|
||||
adPlaybackState = AdPlaybackState.NONE;
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -554,7 +578,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.d(TAG, "onAdEvent: " + adEventType);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Dropping ad event after release: " + adEvent);
|
||||
Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -607,8 +631,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
|
||||
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
|
||||
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
||||
expectedAdGroupIndex =
|
||||
int adGroupIndexForPosition =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||
if (adGroupIndexForPosition != C.INDEX_UNSET) {
|
||||
expectedAdGroupIndex = adGroupIndexForPosition;
|
||||
}
|
||||
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
|
||||
contentPositionMs = player.getCurrentPosition();
|
||||
// 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
|
||||
public void loadAd(String adUriString) {
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Ignoring loadAd after release");
|
||||
return;
|
||||
}
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
Log.w(
|
||||
TAG,
|
||||
|
|
@ -658,9 +713,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
adGroupIndex = expectedAdGroupIndex;
|
||||
adsManager.start();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
|
||||
}
|
||||
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
|
||||
if (adIndexInAdGroup == C.INDEX_UNSET) {
|
||||
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
|
||||
|
|
@ -689,6 +741,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "playAd");
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Ignoring playAd after release");
|
||||
return;
|
||||
}
|
||||
switch (imaAdState) {
|
||||
case IMA_AD_STATE_PLAYING:
|
||||
// IMA does not always call stopAd before resuming content.
|
||||
|
|
@ -732,6 +788,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "stopAd");
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Ignoring stopAd after release");
|
||||
return;
|
||||
}
|
||||
if (player == null) {
|
||||
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
|
||||
Log.w(TAG, "Unexpected stopAd while detached");
|
||||
|
|
@ -771,8 +831,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@Player.TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
|
||||
// The player is being reset and this source will be released.
|
||||
return;
|
||||
|
|
@ -861,8 +921,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
// Internal methods.
|
||||
|
||||
private void startAdPlayback() {
|
||||
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
||||
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
|
||||
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
||||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
||||
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||
|
|
@ -951,11 +1010,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
imaPausedContent = true;
|
||||
pauseContentInternal();
|
||||
break;
|
||||
case STARTED:
|
||||
if (ad.isSkippable()) {
|
||||
focusSkipButton();
|
||||
}
|
||||
break;
|
||||
case TAPPED:
|
||||
if (eventListener != null) {
|
||||
eventListener.onAdTapped();
|
||||
|
|
@ -978,6 +1032,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
handleAdGroupLoadError(new IOException(message));
|
||||
}
|
||||
break;
|
||||
case STARTED:
|
||||
case ALL_ADS_COMPLETED:
|
||||
default:
|
||||
break;
|
||||
|
|
@ -1072,6 +1127,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
if (pendingAdLoadError == null) {
|
||||
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) {
|
||||
|
|
@ -1079,6 +1144,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
Log.d(
|
||||
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
Log.w(TAG, "Ignoring ad prepare error after release");
|
||||
return;
|
||||
}
|
||||
if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
// Send IMA a content position at the ad group so that it will try to play it, at which point
|
||||
// we can notify that it failed to load.
|
||||
|
|
@ -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
|
||||
* 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);
|
||||
// We can't recover from an unexpected error in general, so skip all remaining ads.
|
||||
if (adPlaybackState == null) {
|
||||
adPlaybackState = new AdPlaybackState();
|
||||
adPlaybackState = AdPlaybackState.NONE;
|
||||
} else {
|
||||
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||
|
|
@ -1214,4 +1274,49 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
|||
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.MediaPeriod;
|
||||
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.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
|
@ -34,12 +36,10 @@ import java.io.IOException;
|
|||
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class ImaAdsMediaSource extends BaseMediaSource {
|
||||
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
|
||||
|
||||
private final AdsMediaSource adsMediaSource;
|
||||
|
||||
private SourceInfoRefreshListener adsMediaSourceListener;
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
* {@code contentMediaSource}.
|
||||
|
|
@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
|
||||
adsMediaSourceListener =
|
||||
new SourceInfoRefreshListener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(
|
||||
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||
refreshSourceInfo(timeline, manifest);
|
||||
}
|
||||
};
|
||||
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
|
||||
public void prepareSourceInternal(
|
||||
final ExoPlayer player,
|
||||
boolean isTopLevelSource,
|
||||
@Nullable TransferListener mediaTransferListener) {
|
||||
adsMediaSource.prepareSource(
|
||||
player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
|
|||
|
||||
@Override
|
||||
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
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import com.firebase.jobdispatcher.JobService;
|
|||
import com.firebase.jobdispatcher.Lifetime;
|
||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
|
|
@ -146,11 +147,14 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||
public boolean onStartJob(JobParameters params) {
|
||||
logd("JobDispatcherSchedulerService is started");
|
||||
Bundle extras = params.getExtras();
|
||||
Assertions.checkNotNull(extras, "Service started without extras.");
|
||||
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
|
||||
if (requirements.checkRequirements(this)) {
|
||||
logd("Requirements are met");
|
||||
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
|
||||
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);
|
||||
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
|
||||
Util.startForegroundService(this, intent);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
|||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
|
||||
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
|
||||
public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
||||
public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
|
||||
|
|
@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
private final Player player;
|
||||
private final Handler handler;
|
||||
private final ComponentListener componentListener;
|
||||
private final Runnable updateProgressRunnable;
|
||||
private final int updatePeriodMs;
|
||||
|
||||
private @Nullable PlaybackPreparer playbackPreparer;
|
||||
private ControlDispatcher controlDispatcher;
|
||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
|
||||
private boolean hasSurface;
|
||||
private boolean lastNotifiedPreparedState;
|
||||
|
||||
|
|
@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
this.updatePeriodMs = updatePeriodMs;
|
||||
handler = new Handler();
|
||||
componentListener = new ComponentListener();
|
||||
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);
|
||||
}
|
||||
if (surfaceHolderGlueHost != null) {
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||
removeSurfaceHolderCallback(surfaceHolderGlueHost);
|
||||
surfaceHolderGlueHost = null;
|
||||
}
|
||||
hasSurface = false;
|
||||
|
|
@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
|
||||
@Override
|
||||
public void setProgressUpdatingEnabled(boolean enabled) {
|
||||
handler.removeCallbacks(updateProgressRunnable);
|
||||
handler.removeCallbacks(this);
|
||||
if (enabled) {
|
||||
handler.post(updateProgressRunnable);
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
&& (surfaceHolderGlueHost == null || hasSurface);
|
||||
}
|
||||
|
||||
// Runnable implementation.
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Callback callback = getCallback();
|
||||
callback.onCurrentPositionChanged(this);
|
||||
callback.onBufferedPositionChanged(this);
|
||||
handler.postDelayed(this, updatePeriodMs);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
/* package */ void setVideoSurface(Surface surface) {
|
||||
/* package */ void setVideoSurface(@Nullable Surface surface) {
|
||||
hasSurface = surface != null;
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
|
|
@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
private final class ComponentListener extends Player.DefaultEventListener
|
||||
implements SurfaceHolder.Callback, VideoListener {
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {
|
||||
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
|
||||
}
|
||||
|
||||
private final class ComponentListener
|
||||
implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
|
||||
|
||||
// SurfaceHolder.Callback implementation.
|
||||
|
||||
|
|
@ -281,8 +288,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
Callback callback = getCallback();
|
||||
callback.onDurationChanged(LeanbackPlayerAdapter.this);
|
||||
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -127,7 +127,7 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
|
|||
|
||||
@Override
|
||||
public void onStop(Player player) {
|
||||
player.stop();
|
||||
player.stop(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import android.graphics.Bitmap;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.ResultReceiver;
|
||||
import android.os.SystemClock;
|
||||
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.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -46,25 +46,26 @@ import java.util.Map;
|
|||
|
||||
/**
|
||||
* 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
|
||||
* automatically synced with the player. The connector can also be optionally extended by providing
|
||||
* various collaborators:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
|
||||
* {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
|
||||
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
|
||||
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way.
|
||||
* </li>
|
||||
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
|
||||
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
|
||||
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
|
||||
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
|
||||
* way.
|
||||
* <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
|
||||
* recommended for most use cases.</li>
|
||||
* <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling
|
||||
* {@link #setQueueEditor(QueueEditor)}.</li>
|
||||
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
|
||||
* is recommended for most use cases.
|
||||
* <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
|
||||
* #setQueueEditor(QueueEditor)}.
|
||||
* <li>An {@link ErrorMessageProvider} for providing human readable error messages and
|
||||
* corresponding error codes can be set by calling
|
||||
* {@link #setErrorMessageProvider(ErrorMessageProvider)}.</li>
|
||||
* corresponding error codes can be set by calling {@link
|
||||
* #setErrorMessageProvider(ErrorMessageProvider)}.
|
||||
* </ul>
|
||||
*/
|
||||
public final class MediaSessionConnector {
|
||||
|
|
@ -74,35 +75,30 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
|
||||
/**
|
||||
* The default repeat toggle modes which is the bitmask of
|
||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and
|
||||
* {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
|
||||
* The default repeat toggle modes which is the bitmask of {@link
|
||||
* RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
|
||||
*/
|
||||
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
|
||||
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;
|
||||
|
||||
/**
|
||||
* Receiver of media commands sent by a media controller.
|
||||
*/
|
||||
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;
|
||||
|
||||
/** Receiver of media commands sent by a media controller. */
|
||||
public interface CommandReceiver {
|
||||
/**
|
||||
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to which playback preparation actions are delegated.
|
||||
*/
|
||||
/** Interface to which playback preparation actions are delegated. */
|
||||
public interface PlaybackPreparer extends CommandReceiver {
|
||||
|
||||
long ACTIONS =
|
||||
|
|
@ -127,96 +123,77 @@ public final class MediaSessionConnector {
|
|||
* @return The bitmask of the supported media actions.
|
||||
*/
|
||||
long getSupportedPrepareActions();
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepare()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
|
||||
void onPrepare();
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
|
||||
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);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
|
||||
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 {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
|
||||
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
|
||||
| PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
|
||||
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
||||
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
|
||||
long ACTIONS =
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||
| PlaybackStateCompat.ACTION_PLAY
|
||||
| PlaybackStateCompat.ACTION_PAUSE
|
||||
| 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
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
|
||||
* {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
|
||||
* {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
|
||||
* {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP},
|
||||
* {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and
|
||||
* {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link
|
||||
* PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link
|
||||
* PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link
|
||||
* PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link
|
||||
* PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link
|
||||
* PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
|
||||
*
|
||||
* @param player The player.
|
||||
* @return The bitmask of the supported media actions.
|
||||
*/
|
||||
long getSupportedPlaybackActions(@Nullable Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPlay()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onPlay()}. */
|
||||
void onPlay(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPause()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onPause()}. */
|
||||
void onPause(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSeekTo(long)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */
|
||||
void onSeekTo(Player player, long position);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onFastForward()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onFastForward()}. */
|
||||
void onFastForward(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onRewind()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onRewind()}. */
|
||||
void onRewind(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onStop()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onStop()}. */
|
||||
void onStop(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */
|
||||
void onSetShuffleMode(Player player, int shuffleMode);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */
|
||||
void onSetRepeatMode(Player player, int repeatMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles queue navigation actions, and updates the media session queue by calling
|
||||
* {@code MediaSessionCompat.setQueue()}.
|
||||
* Handles queue navigation actions, and updates the media session queue by calling {@code
|
||||
* MediaSessionCompat.setQueue()}.
|
||||
*/
|
||||
public interface QueueNavigator extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||
long ACTIONS =
|
||||
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
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
|
||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
|
||||
* {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link
|
||||
* PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
|
||||
* PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
|
||||
*
|
||||
* @param player The {@link Player}.
|
||||
* @return The bitmask of the supported media actions.
|
||||
|
|
@ -235,34 +212,26 @@ public final class MediaSessionConnector {
|
|||
*/
|
||||
void onCurrentWindowIndexChanged(Player player);
|
||||
/**
|
||||
* Gets the id of the currently active queue item, or
|
||||
* {@link 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
|
||||
* returned id must be available in the list of items returned by
|
||||
* {@link MediaControllerCompat#getQueue()}.
|
||||
* Gets the id of the currently active queue item, or {@link
|
||||
* 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
|
||||
* returned id must be available in the list of items returned by {@link
|
||||
* MediaControllerCompat#getQueue()}.
|
||||
*
|
||||
* @param player The player connected to the media session.
|
||||
* @return The id of the active queue item.
|
||||
*/
|
||||
long getActiveQueueItemId(@Nullable Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */
|
||||
void onSkipToPrevious(Player player);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */
|
||||
void onSkipToQueueItem(Player player, long id);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onSkipToNext()}.
|
||||
*/
|
||||
/** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */
|
||||
void onSkipToNext(Player player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles media session queue edits.
|
||||
*/
|
||||
/** Handles media session queue edits. */
|
||||
public interface QueueEditor extends CommandReceiver {
|
||||
|
||||
/**
|
||||
|
|
@ -270,8 +239,8 @@ public final class MediaSessionConnector {
|
|||
*/
|
||||
void onAddQueueItem(Player player, MediaDescriptionCompat description);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
|
||||
* int index)}.
|
||||
* See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int
|
||||
* index)}.
|
||||
*/
|
||||
void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
|
||||
/**
|
||||
|
|
@ -279,9 +248,7 @@ public final class MediaSessionConnector {
|
|||
* 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);
|
||||
}
|
||||
|
||||
|
|
@ -308,43 +275,49 @@ public final class MediaSessionConnector {
|
|||
void onCustomAction(String action, Bundle extras);
|
||||
|
||||
/**
|
||||
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the
|
||||
* media session by the connector or {@code null} if this action should not be published at the
|
||||
* given player state.
|
||||
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
|
||||
* session by the connector or {@code null} if this action should not be published at the given
|
||||
* player state.
|
||||
*
|
||||
* @return The custom action to be included in the session playback state or {@code null}.
|
||||
*/
|
||||
PlaybackStateCompat.CustomAction getCustomAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrapped {@link MediaSessionCompat}.
|
||||
*/
|
||||
/** Provides a {@link MediaMetadataCompat} for a given player state. */
|
||||
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;
|
||||
|
||||
private final MediaControllerCompat mediaController;
|
||||
private final Handler handler;
|
||||
private final boolean doMaintainMetadata;
|
||||
private @Nullable final MediaMetadataProvider mediaMetadataProvider;
|
||||
private final ExoPlayerEventListener exoPlayerEventListener;
|
||||
private final MediaSessionCallback mediaSessionCallback;
|
||||
private final PlaybackController playbackController;
|
||||
private final String metadataExtrasPrefix;
|
||||
private final Map<String, CommandReceiver> commandMap;
|
||||
|
||||
private Player player;
|
||||
private CustomActionProvider[] customActionProviders;
|
||||
private Map<String, CustomActionProvider> customActionMap;
|
||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private @Nullable Pair<Integer, CharSequence> customError;
|
||||
private PlaybackPreparer playbackPreparer;
|
||||
private QueueNavigator queueNavigator;
|
||||
private QueueEditor queueEditor;
|
||||
private RatingCallback ratingCallback;
|
||||
|
||||
/**
|
||||
* Creates an instance. Must be called on the same thread that is used to construct the player
|
||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
||||
* <p>
|
||||
* Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
|
||||
*
|
||||
* @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
|
||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <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 playbackController A {@link PlaybackController} for handling playback actions.
|
||||
*/
|
||||
public MediaSessionConnector(
|
||||
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 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. If
|
||||
* {@code false}, you need to maintain the metadata of the media session yourself (provide at
|
||||
* least the duration to allow clients to show a progress bar).
|
||||
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
|
||||
* queue item to the session metadata.
|
||||
* @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
|
||||
* object to be published to the media session, or {@code null} if metadata shouldn't be
|
||||
* published.
|
||||
*/
|
||||
public MediaSessionConnector(
|
||||
MediaSessionCompat mediaSession,
|
||||
PlaybackController playbackController,
|
||||
boolean doMaintainMetadata,
|
||||
@Nullable String metadataExtrasPrefix) {
|
||||
@Nullable PlaybackController playbackController,
|
||||
@Nullable MediaMetadataProvider mediaMetadataProvider) {
|
||||
this.mediaSession = mediaSession;
|
||||
this.playbackController = playbackController != null ? playbackController
|
||||
: new DefaultPlaybackController();
|
||||
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
|
||||
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
|
||||
: Looper.getMainLooper());
|
||||
this.doMaintainMetadata = doMaintainMetadata;
|
||||
this.playbackController =
|
||||
playbackController != null ? playbackController : new DefaultPlaybackController();
|
||||
this.mediaMetadataProvider = mediaMetadataProvider;
|
||||
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
||||
mediaController = mediaSession.getController();
|
||||
mediaSessionCallback = new MediaSessionCallback();
|
||||
exoPlayerEventListener = new ExoPlayerEventListener();
|
||||
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
|
||||
* actions published with the playback state of the session.
|
||||
|
|
@ -425,14 +421,17 @@ public final class MediaSessionConnector {
|
|||
this.playbackPreparer = playbackPreparer;
|
||||
registerCommandReceiver(playbackPreparer);
|
||||
|
||||
this.customActionProviders = (player != null && customActionProviders != null)
|
||||
? customActionProviders : new CustomActionProvider[0];
|
||||
this.customActionProviders =
|
||||
(player != null && customActionProviders != null)
|
||||
? customActionProviders
|
||||
: new CustomActionProvider[0];
|
||||
if (player != null) {
|
||||
Handler handler = new Handler(Util.getLooper());
|
||||
mediaSession.setCallback(mediaSessionCallback, handler);
|
||||
player.addListener(exoPlayerEventListener);
|
||||
}
|
||||
updateMediaSessionPlaybackState();
|
||||
updateMediaSessionMetadata();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -444,7 +443,7 @@ public final class MediaSessionConnector {
|
|||
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
|
||||
if (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) {
|
||||
for (String command : commandReceiver.getCommands()) {
|
||||
commandMap.put(command, commandReceiver);
|
||||
}
|
||||
/**
|
||||
* Sets a custom error on the session.
|
||||
*
|
||||
* <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) {
|
||||
for (String command : commandReceiver.getCommands()) {
|
||||
commandMap.remove(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMediaSessionPlaybackState() {
|
||||
/**
|
||||
* Updates the playback state of the media session.
|
||||
*
|
||||
* <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() {
|
||||
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
||||
if (player == null) {
|
||||
builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
|
||||
|
|
@ -527,36 +553,74 @@ public final class MediaSessionConnector {
|
|||
int playbackState = player.getPlaybackState();
|
||||
ExoPlaybackException playbackError =
|
||||
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
||||
boolean reportError = playbackError != null || customError != null;
|
||||
int sessionPlaybackState =
|
||||
playbackError != null
|
||||
reportError
|
||||
? PlaybackStateCompat.STATE_ERROR
|
||||
: 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);
|
||||
builder.setErrorMessage(message.first, message.second);
|
||||
}
|
||||
long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
|
||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
long activeQueueItemId =
|
||||
queueNavigator != null
|
||||
? queueNavigator.getActiveQueueItemId(player)
|
||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
Bundle extras = new Bundle();
|
||||
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
|
||||
builder.setActions(buildPlaybackActions())
|
||||
builder
|
||||
.setActions(buildPlaybackActions())
|
||||
.setActiveQueueItemId(activeQueueItemId)
|
||||
.setBufferedPosition(player.getBufferedPosition())
|
||||
.setState(sessionPlaybackState, player.getCurrentPosition(),
|
||||
player.getPlaybackParameters().speed, SystemClock.elapsedRealtime())
|
||||
.setState(
|
||||
sessionPlaybackState,
|
||||
player.getCurrentPosition(),
|
||||
player.getPlaybackParameters().speed,
|
||||
SystemClock.elapsedRealtime())
|
||||
.setExtras(extras);
|
||||
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() {
|
||||
long actions = (PlaybackController.ACTIONS
|
||||
& playbackController.getSupportedPlaybackActions(player));
|
||||
long actions =
|
||||
(PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
|
||||
if (playbackPreparer != null) {
|
||||
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
||||
}
|
||||
if (queueNavigator != null) {
|
||||
actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
|
||||
player));
|
||||
actions |=
|
||||
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
|
||||
}
|
||||
if (ratingCallback != null) {
|
||||
actions |= RatingCallback.ACTIONS;
|
||||
|
|
@ -564,17 +628,79 @@ public final class MediaSessionConnector {
|
|||
return actions;
|
||||
}
|
||||
|
||||
private void updateMediaSessionMetadata() {
|
||||
if (doMaintainMetadata) {
|
||||
private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
|
||||
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();
|
||||
if (player != null && player.isPlayingAd()) {
|
||||
if (player.isPlayingAd()) {
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
|
||||
}
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0
|
||||
: player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
|
||||
|
||||
if (queueNavigator != null) {
|
||||
long activeQueueItemId = queueNavigator.getActiveQueueItemId(player);
|
||||
builder.putLong(
|
||||
MediaMetadataCompat.METADATA_KEY_DURATION,
|
||||
player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
|
||||
long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
|
||||
if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
|
||||
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
|
||||
for (int i = 0; queue != null && i < queue.size(); i++) {
|
||||
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
||||
|
|
@ -600,113 +726,92 @@ public final class MediaSessionConnector {
|
|||
}
|
||||
}
|
||||
if (description.getTitle() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
|
||||
String.valueOf(description.getTitle()));
|
||||
String title = String.valueOf(description.getTitle());
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
|
||||
}
|
||||
if (description.getSubtitle() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
||||
builder.putString(
|
||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
||||
String.valueOf(description.getSubtitle()));
|
||||
}
|
||||
if (description.getDescription() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||
builder.putString(
|
||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||
String.valueOf(description.getDescription()));
|
||||
}
|
||||
if (description.getIconBitmap() != null) {
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
|
||||
description.getIconBitmap());
|
||||
builder.putBitmap(
|
||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
|
||||
}
|
||||
if (description.getIconUri() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
|
||||
builder.putString(
|
||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
|
||||
String.valueOf(description.getIconUri()));
|
||||
}
|
||||
if (description.getMediaId() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
|
||||
builder.putString(
|
||||
MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
|
||||
String.valueOf(description.getMediaId()));
|
||||
}
|
||||
if (description.getMediaUri() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
|
||||
builder.putString(
|
||||
MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
|
||||
String.valueOf(description.getMediaUri()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaSession.setMetadata(builder.build());
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
|
||||
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 class ExoPlayerEventListener implements Player.EventListener {
|
||||
|
||||
private int currentWindowIndex;
|
||||
private int currentWindowCount;
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest,
|
||||
@Player.TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
|
||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||
int windowIndex = player.getCurrentWindowIndex();
|
||||
if (queueNavigator != null) {
|
||||
queueNavigator.onTimelineChanged(player);
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
|
||||
// active queue item and queue navigation actions may need to be updated
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
currentWindowCount = windowCount;
|
||||
currentWindowIndex = windowIndex;
|
||||
updateMediaSessionMetadata();
|
||||
invalidateMediaSessionMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||
mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE
|
||||
? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL
|
||||
? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE);
|
||||
updateMediaSessionPlaybackState();
|
||||
mediaSession.setRepeatMode(
|
||||
repeatMode == Player.REPEAT_MODE_ONE
|
||||
? PlaybackStateCompat.REPEAT_MODE_ONE
|
||||
: repeatMode == Player.REPEAT_MODE_ALL
|
||||
? PlaybackStateCompat.REPEAT_MODE_ALL
|
||||
: PlaybackStateCompat.REPEAT_MODE_NONE);
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
||||
updateMediaSessionPlaybackState();
|
||||
mediaSession.setShuffleMode(
|
||||
shuffleModeEnabled
|
||||
? PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -716,16 +821,19 @@ public final class MediaSessionConnector {
|
|||
queueNavigator.onCurrentWindowIndexChanged(player);
|
||||
}
|
||||
currentWindowIndex = player.getCurrentWindowIndex();
|
||||
updateMediaSessionMetadata();
|
||||
// Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
|
||||
// and before updating metadata.
|
||||
invalidateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionMetadata();
|
||||
return;
|
||||
}
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MediaSessionCallback extends MediaSessionCompat.Callback {
|
||||
|
|
@ -812,7 +920,7 @@ public final class MediaSessionConnector {
|
|||
Map<String, CustomActionProvider> actionMap = customActionMap;
|
||||
if (actionMap.containsKey(action)) {
|
||||
actionMap.get(action).onCustomAction(action, extras);
|
||||
updateMediaSessionPlaybackState();
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -921,7 +1029,5 @@ public final class MediaSessionConnector {
|
|||
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");
|
||||
* 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");
|
||||
* 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) {
|
||||
if (player.getCurrentTimeline().isEmpty()) {
|
||||
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
|
||||
mediaSession.setQueue(Collections.emptyList());
|
||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
@ -28,7 +33,8 @@ android {
|
|||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -15,24 +15,25 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.okhttp;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.DataSpec;
|
||||
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.Predicate;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
|
|
@ -40,30 +41,28 @@ import okhttp3.MediaType;
|
|||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
|
||||
*/
|
||||
public class OkHttpDataSource implements HttpDataSource {
|
||||
/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */
|
||||
public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
static {
|
||||
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;
|
||||
@NonNull private final RequestProperties requestProperties;
|
||||
private final Call.Factory callFactory;
|
||||
private final RequestProperties requestProperties;
|
||||
|
||||
@Nullable private final String userAgent;
|
||||
@Nullable private final Predicate<String> contentTypePredicate;
|
||||
@Nullable private final TransferListener<? super OkHttpDataSource> listener;
|
||||
@Nullable private final CacheControl cacheControl;
|
||||
@Nullable private final RequestProperties defaultRequestProperties;
|
||||
private final @Nullable String userAgent;
|
||||
private final @Nullable Predicate<String> contentTypePredicate;
|
||||
private final @Nullable CacheControl cacheControl;
|
||||
private final @Nullable RequestProperties defaultRequestProperties;
|
||||
|
||||
private DataSpec dataSpec;
|
||||
private Response response;
|
||||
private InputStream responseByteStream;
|
||||
private @Nullable DataSpec dataSpec;
|
||||
private @Nullable Response response;
|
||||
private @Nullable InputStream responseByteStream;
|
||||
private boolean opened;
|
||||
|
||||
private long bytesToSkip;
|
||||
|
|
@ -77,11 +76,19 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
* 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 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) {
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
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.
|
||||
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
|
||||
* #open(DataSpec)}.
|
||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||
* @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 TransferListener<? super OkHttpDataSource> listener,
|
||||
@Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
|
||||
@Nullable CacheControl cacheControl,
|
||||
@Nullable RequestProperties defaultRequestProperties) {
|
||||
super(/* isNetwork= */ true);
|
||||
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||
this.userAgent = userAgent;
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.listener = listener;
|
||||
this.cacheControl = cacheControl;
|
||||
this.defaultRequestProperties = defaultRequestProperties;
|
||||
this.requestProperties = new RequestProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
public @Nullable Uri getUri() {
|
||||
return response == null ? null : Uri.parse(response.request().url().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return response == null ? null : response.headers().toMultimap();
|
||||
return response == null ? Collections.emptyMap() : response.headers().toMultimap();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -157,10 +150,16 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
this.dataSpec = dataSpec;
|
||||
this.bytesRead = 0;
|
||||
this.bytesSkipped = 0;
|
||||
transferInitializing(dataSpec);
|
||||
|
||||
Request request = makeRequest(dataSpec);
|
||||
Response response;
|
||||
ResponseBody responseBody;
|
||||
try {
|
||||
response = callFactory.newCall(request).execute();
|
||||
responseByteStream = response.body().byteStream();
|
||||
this.response = callFactory.newCall(request).execute();
|
||||
response = this.response;
|
||||
responseBody = Assertions.checkNotNull(response.body());
|
||||
responseByteStream = responseBody.byteStream();
|
||||
} catch (IOException e) {
|
||||
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
|
||||
dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
|
|
@ -181,8 +180,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
// Check for a valid content type.
|
||||
MediaType mediaType = response.body().contentType();
|
||||
String contentType = mediaType != null ? mediaType.toString() : null;
|
||||
MediaType mediaType = responseBody.contentType();
|
||||
String contentType = mediaType != null ? mediaType.toString() : "";
|
||||
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
|
||||
closeConnectionQuietly();
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
|
|
@ -197,14 +196,12 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesToRead = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = response.body().contentLength();
|
||||
long contentLength = responseBody.contentLength();
|
||||
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
if (listener != null) {
|
||||
listener.onTransferStart(this, dataSpec);
|
||||
}
|
||||
transferStarted(dataSpec);
|
||||
|
||||
return bytesToRead;
|
||||
}
|
||||
|
|
@ -215,7 +212,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
skipInternal();
|
||||
return readInternal(buffer, offset, readLength);
|
||||
} 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 {
|
||||
if (opened) {
|
||||
opened = false;
|
||||
if (listener != null) {
|
||||
listener.onTransferEnd(this);
|
||||
}
|
||||
transferEnded();
|
||||
closeConnectionQuietly();
|
||||
}
|
||||
}
|
||||
|
|
@ -262,15 +258,18 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a connection.
|
||||
*/
|
||||
private Request makeRequest(DataSpec dataSpec) {
|
||||
/** Establishes a connection. */
|
||||
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
|
||||
long position = dataSpec.position;
|
||||
long length = dataSpec.length;
|
||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||
|
||||
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);
|
||||
if (cacheControl != null) {
|
||||
builder.cacheControl(cacheControl);
|
||||
|
|
@ -297,9 +296,14 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
if (!allowGzip) {
|
||||
builder.addHeader("Accept-Encoding", "identity");
|
||||
}
|
||||
if (dataSpec.postBody != null) {
|
||||
builder.post(RequestBody.create(null, dataSpec.postBody));
|
||||
RequestBody requestBody = null;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -316,15 +320,9 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
return;
|
||||
}
|
||||
|
||||
// Acquire the shared skip buffer.
|
||||
byte[] skipBuffer = skipBufferReference.getAndSet(null);
|
||||
if (skipBuffer == null) {
|
||||
skipBuffer = new byte[4096];
|
||||
}
|
||||
|
||||
while (bytesSkipped != bytesToSkip) {
|
||||
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
|
||||
int read = responseByteStream.read(skipBuffer, 0, readLength);
|
||||
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
|
||||
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
|
|
@ -332,13 +330,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
throw new EOFException();
|
||||
}
|
||||
bytesSkipped += read;
|
||||
if (listener != null) {
|
||||
listener.onBytesTransferred(this, read);
|
||||
}
|
||||
bytesTransferred(read);
|
||||
}
|
||||
|
||||
// Release the shared skip buffer.
|
||||
skipBufferReference.set(skipBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -367,7 +360,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
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 (bytesToRead != C.LENGTH_UNSET) {
|
||||
// End of stream reached having not read sufficient data.
|
||||
|
|
@ -377,9 +370,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
bytesRead += read;
|
||||
if (listener != null) {
|
||||
listener.onBytesTransferred(this, read);
|
||||
}
|
||||
bytesTransferred(read);
|
||||
return read;
|
||||
}
|
||||
|
||||
|
|
@ -387,8 +378,10 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
* Closes the current connection quietly, if there is one.
|
||||
*/
|
||||
private void closeConnectionQuietly() {
|
||||
response.body().close();
|
||||
response = null;
|
||||
if (response != null) {
|
||||
Assertions.checkNotNull(response.body()).close();
|
||||
response = null;
|
||||
}
|
||||
responseByteStream = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.okhttp;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
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.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
|
|
@ -30,10 +28,30 @@ import okhttp3.Call;
|
|||
*/
|
||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||
|
||||
@NonNull private final Call.Factory callFactory;
|
||||
@Nullable private final String userAgent;
|
||||
@Nullable private final TransferListener<? super DataSource> listener;
|
||||
@Nullable private final CacheControl cacheControl;
|
||||
private final Call.Factory callFactory;
|
||||
private final @Nullable String userAgent;
|
||||
private final @Nullable TransferListener listener;
|
||||
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
|
||||
|
|
@ -41,9 +59,9 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
|||
* @param userAgent An optional User-Agent string.
|
||||
* @param listener An optional listener.
|
||||
*/
|
||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable TransferListener<? super DataSource> listener) {
|
||||
this(callFactory, userAgent, listener, null);
|
||||
public OkHttpDataSourceFactory(
|
||||
Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) {
|
||||
this(callFactory, userAgent, listener, /* cacheControl= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,8 +71,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
|||
* @param listener An optional listener.
|
||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||
*/
|
||||
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||
@Nullable TransferListener<? super DataSource> listener,
|
||||
public OkHttpDataSourceFactory(
|
||||
Call.Factory callFactory,
|
||||
@Nullable String userAgent,
|
||||
@Nullable TransferListener listener,
|
||||
@Nullable CacheControl cacheControl) {
|
||||
this.callFactory = callFactory;
|
||||
this.userAgent = userAgent;
|
||||
|
|
@ -65,8 +85,16 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
|||
@Override
|
||||
protected OkHttpDataSource createDataSourceInternal(
|
||||
HttpDataSource.RequestProperties defaultRequestProperties) {
|
||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
|
||||
defaultRequestProperties);
|
||||
OkHttpDataSource dataSource =
|
||||
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
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -64,8 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||
*/
|
||||
public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
|
||||
/** The number of input and output buffers. */
|
||||
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;
|
||||
|
||||
|
|
@ -88,8 +90,15 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
@Override
|
||||
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws OpusDecoderException {
|
||||
decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||
format.initializationData, mediaCrypto);
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
decoder =
|
||||
new OpusDecoder(
|
||||
NUM_BUFFERS,
|
||||
NUM_BUFFERS,
|
||||
initialInputBufferSize,
|
||||
format.initializationData,
|
||||
mediaCrypto);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
|||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.DataSpec;
|
||||
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.RtmpIOException;
|
||||
|
||||
/**
|
||||
* A Real-Time Messaging Protocol (RTMP) {@link DataSource}.
|
||||
*/
|
||||
public final class RtmpDataSource implements DataSource {
|
||||
/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */
|
||||
public final class RtmpDataSource extends BaseDataSource {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
|
||||
}
|
||||
|
||||
@Nullable private final TransferListener<? super RtmpDataSource> listener;
|
||||
|
||||
private RtmpClient rtmpClient;
|
||||
private Uri uri;
|
||||
|
||||
public RtmpDataSource() {
|
||||
this(null);
|
||||
super(/* isNetwork= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listener An optional listener.
|
||||
* @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
|
||||
*/
|
||||
public RtmpDataSource(@Nullable TransferListener<? super RtmpDataSource> listener) {
|
||||
this.listener = listener;
|
||||
@Deprecated
|
||||
public RtmpDataSource(@Nullable TransferListener listener) {
|
||||
this();
|
||||
if (listener != null) {
|
||||
addTransferListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws RtmpIOException {
|
||||
transferInitializing(dataSpec);
|
||||
rtmpClient = new RtmpClient();
|
||||
rtmpClient.open(dataSpec.uri.toString(), false);
|
||||
|
||||
this.uri = dataSpec.uri;
|
||||
if (listener != null) {
|
||||
listener.onTransferStart(this, dataSpec);
|
||||
}
|
||||
transferStarted(dataSpec);
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -69,9 +70,7 @@ public final class RtmpDataSource implements DataSource {
|
|||
if (bytesRead == -1) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onBytesTransferred(this, bytesRead);
|
||||
}
|
||||
bytesTransferred(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +78,7 @@ public final class RtmpDataSource implements DataSource {
|
|||
public void close() {
|
||||
if (uri != null) {
|
||||
uri = null;
|
||||
if (listener != null) {
|
||||
listener.onTransferEnd(this);
|
||||
}
|
||||
transferEnded();
|
||||
}
|
||||
if (rtmpClient != null) {
|
||||
rtmpClient.close();
|
||||
|
|
|
|||
|
|
@ -25,17 +25,14 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
|||
*/
|
||||
public final class RtmpDataSourceFactory implements DataSource.Factory {
|
||||
|
||||
@Nullable
|
||||
private final TransferListener<? super RtmpDataSource> listener;
|
||||
private final @Nullable TransferListener listener;
|
||||
|
||||
public RtmpDataSourceFactory() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listener An optional listener.
|
||||
*/
|
||||
public RtmpDataSourceFactory(@Nullable TransferListener<? super RtmpDataSource> listener) {
|
||||
/** @param listener An optional listener. */
|
||||
public RtmpDataSourceFactory(@Nullable TransferListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
|
|
|
|||
|
|
@ -95,8 +95,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||
|
||||
private final Context context;
|
||||
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.
|
||||
*/
|
||||
private static final int NUM_OUTPUT_BUFFERS = 8;
|
||||
/**
|
||||
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
|
||||
* insufficient.
|
||||
*/
|
||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
||||
/** The default input buffer size. */
|
||||
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
||||
|
||||
private final boolean scaleToFit;
|
||||
private final boolean disableLoopFilter;
|
||||
|
|
@ -114,6 +111,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private final FormatHolder formatHolder;
|
||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
private final boolean useSurfaceYuvOutput;
|
||||
|
||||
private Format format;
|
||||
private VpxDecoder decoder;
|
||||
|
|
@ -177,7 +175,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
maxDroppedFramesToNotify,
|
||||
/* drmSessionManager= */ null,
|
||||
/* 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}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @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,
|
||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) {
|
||||
public LibvpxVideoRenderer(
|
||||
boolean scaleToFit,
|
||||
long allowedJoiningTimeMs,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify,
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean disableLoopFilter,
|
||||
boolean useSurfaceYuvOutput) {
|
||||
super(C.TRACK_TYPE_VIDEO);
|
||||
this.scaleToFit = scaleToFit;
|
||||
this.disableLoopFilter = disableLoopFilter;
|
||||
|
|
@ -209,6 +215,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
this.useSurfaceYuvOutput = useSurfaceYuvOutput;
|
||||
joiningDeadlineMs = C.TIME_UNSET;
|
||||
clearReportedVideoSize();
|
||||
formatHolder = new FormatHolder();
|
||||
|
|
@ -549,21 +556,25 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
*
|
||||
* @param outputBuffer The buffer to render.
|
||||
*/
|
||||
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) {
|
||||
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
|
||||
int bufferMode = outputBuffer.mode;
|
||||
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;
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
if (!renderRgb && !renderYuv) {
|
||||
if (!renderRgb && !renderYuv && !renderSurface) {
|
||||
dropOutputBuffer(outputBuffer);
|
||||
} else {
|
||||
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
||||
if (renderRgb) {
|
||||
renderRgbFrame(outputBuffer, scaleToFit);
|
||||
outputBuffer.release();
|
||||
} else /* renderYuv */ {
|
||||
} else if (renderYuv) {
|
||||
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
||||
// The renderer will release the buffer.
|
||||
} else { // renderSurface
|
||||
decoder.renderToSurface(outputBuffer, surface);
|
||||
outputBuffer.release();
|
||||
}
|
||||
consecutiveDroppedFrameCount = 0;
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
|
|
@ -633,8 +644,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
// The output has changed.
|
||||
this.surface = surface;
|
||||
this.outputBufferRenderer = outputBufferRenderer;
|
||||
outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV
|
||||
: surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE;
|
||||
if (surface != null) {
|
||||
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 (decoder != null) {
|
||||
decoder.setOutputMode(outputMode);
|
||||
|
|
@ -684,13 +700,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
try {
|
||||
long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createVpxDecoder");
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
decoder =
|
||||
new VpxDecoder(
|
||||
NUM_INPUT_BUFFERS,
|
||||
NUM_OUTPUT_BUFFERS,
|
||||
INITIAL_INPUT_BUFFER_SIZE,
|
||||
initialInputBufferSize,
|
||||
mediaCrypto,
|
||||
disableLoopFilter);
|
||||
disableLoopFilter,
|
||||
useSurfaceYuvOutput);
|
||||
decoder.setOutputMode(outputMode);
|
||||
TraceUtil.endSection();
|
||||
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
|
|
@ -817,7 +836,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
||||
*/
|
||||
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
||||
throws ExoPlaybackException {
|
||||
throws ExoPlaybackException, VpxDecoderException {
|
||||
if (initialPositionUs == C.TIME_UNSET) {
|
||||
initialPositionUs = positionUs;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
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_YUV = 0;
|
||||
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 DECODE_ERROR = 1;
|
||||
|
|
@ -50,10 +52,17 @@ import java.nio.ByteBuffer;
|
|||
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
||||
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||
* @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.
|
||||
*/
|
||||
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||
ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException {
|
||||
public VpxDecoder(
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers,
|
||||
int initialInputBufferSize,
|
||||
ExoMediaCrypto exoMediaCrypto,
|
||||
boolean disableLoopFilter,
|
||||
boolean enableSurfaceYuvOutputMode)
|
||||
throws VpxDecoderException {
|
||||
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||
if (!VpxLibrary.isAvailable()) {
|
||||
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
||||
|
|
@ -62,7 +71,7 @@ import java.nio.ByteBuffer;
|
|||
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
||||
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
||||
}
|
||||
vpxDecContext = vpxInit(disableLoopFilter);
|
||||
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
|
||||
if (vpxDecContext == 0) {
|
||||
throw new VpxDecoderException("Failed to initialize decoder");
|
||||
}
|
||||
|
|
@ -96,6 +105,11 @@ import java.nio.ByteBuffer;
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -145,13 +159,36 @@ import java.nio.ByteBuffer;
|
|||
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 vpxDecode(long context, ByteBuffer encoded, int length);
|
||||
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
|
||||
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
|
||||
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||
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 String vpxGetErrorMessage(long context);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import java.nio.ByteBuffer;
|
|||
public static final int COLORSPACE_BT2020 = 3;
|
||||
|
||||
private final VpxDecoder owner;
|
||||
/** Decoder private data. */
|
||||
public int decoderPrivate;
|
||||
|
||||
public int mode;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ LOCAL_MODULE := libvpxJNI
|
|||
LOCAL_ARM_MODE := arm
|
||||
LOCAL_CPP_EXTENSION := .cc
|
||||
LOCAL_SRC_FILES := vpx_jni.cc
|
||||
LOCAL_LDLIBS := -llog -lz -lm
|
||||
LOCAL_LDLIBS := -llog -lz -lm -landroid
|
||||
LOCAL_SHARED_LIBRARIES := libvpx
|
||||
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
#include <pthread.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
|
@ -63,6 +65,11 @@ static jmethodID initForRgbFrame;
|
|||
static jmethodID initForYuvFrame;
|
||||
static jfieldID dataField;
|
||||
static jfieldID outputModeField;
|
||||
static jfieldID decoderPrivateField;
|
||||
|
||||
// android.graphics.ImageFormat.YV12.
|
||||
static const int kHalPixelFormatYV12 = 0x32315659;
|
||||
static const int kDecoderPrivateBase = 0x100;
|
||||
static int errorCode;
|
||||
|
||||
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) {
|
||||
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
|
||||
struct JniFrameBuffer {
|
||||
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};
|
||||
cfg.threads = android_getCpuCount();
|
||||
errorCode = 0;
|
||||
vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
|
||||
&cfg, 0);
|
||||
vpx_codec_err_t err =
|
||||
vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);
|
||||
if (err) {
|
||||
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
|
||||
errorCode = err;
|
||||
|
|
@ -296,11 +456,20 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
|||
}
|
||||
if (disableLoopFilter) {
|
||||
// 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) {
|
||||
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.
|
||||
const jclass outputBufferClass = env->FindClass(
|
||||
|
|
@ -312,16 +481,17 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
|||
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||
"Ljava/nio/ByteBuffer;");
|
||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||
|
||||
decoderPrivateField =
|
||||
env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
|
||||
return reinterpret_cast<intptr_t>(context);
|
||||
}
|
||||
|
||||
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 =
|
||||
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
|
||||
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;
|
||||
if (status != VPX_CODEC_OK) {
|
||||
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) {
|
||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
||||
vpx_codec_destroy(context);
|
||||
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||
vpx_codec_destroy(context->decoder);
|
||||
delete context;
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
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) {
|
||||
return 1;
|
||||
|
|
@ -360,6 +530,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||
|
||||
const int kOutputModeYuv = 0;
|
||||
const int kOutputModeRgb = 1;
|
||||
const int kOutputModeSurfaceYuv = 2;
|
||||
|
||||
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
||||
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 + 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
||||
return env->NewStringUTF(vpx_codec_error(context));
|
||||
JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);
|
||||
return env->NewStringUTF(vpx_codec_error(context->decoder));
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }
|
||||
|
|
|
|||
|
|
@ -18,10 +18,22 @@ android {
|
|||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
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.
|
||||
|
|
@ -47,10 +59,14 @@ android {
|
|||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
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 'junit:junit:' + junitVersion
|
||||
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;
|
||||
|
||||
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.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
|
|
@ -28,48 +28,58 @@ import android.os.Bundle;
|
|||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
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.testutil.TestUtil;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContentDataSource}.
|
||||
*/
|
||||
public final class ContentDataSourceTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link ContentDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ContentDataSourceTest {
|
||||
|
||||
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
|
||||
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
|
||||
|
||||
@Test
|
||||
public void testRead() throws Exception {
|
||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false);
|
||||
assertData(0, C.LENGTH_UNSET, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPipeMode() throws Exception {
|
||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true);
|
||||
assertData(0, C.LENGTH_UNSET, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFixedLength() throws Exception {
|
||||
assertData(getInstrumentation(), 0, 100, false);
|
||||
assertData(0, 100, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetToEndOfInput() throws Exception {
|
||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false);
|
||||
assertData(1, C.LENGTH_UNSET, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
|
||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true);
|
||||
assertData(1, C.LENGTH_UNSET, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetFixedLength() throws Exception {
|
||||
assertData(getInstrumentation(), 1, 100, false);
|
||||
assertData(1, 100, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
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);
|
||||
DataSpec dataSpec = new DataSpec(contentUri);
|
||||
try {
|
||||
|
|
@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private static void assertData(Instrumentation instrumentation, int offset, int length,
|
||||
boolean pipeMode) throws IOException {
|
||||
private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
|
||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
|
||||
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
|
||||
ContentDataSource dataSource =
|
||||
new ContentDataSource(InstrumentationRegistry.getTargetContext());
|
||||
try {
|
||||
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,
|
||||
length == C.LENGTH_UNSET ? completeData.length : offset + length);
|
||||
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 android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.SparseArray;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
|
|
@ -29,9 +30,14 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests {@link CachedContentIndex}. */
|
||||
public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class CachedContentIndexTest {
|
||||
|
||||
private final byte[] testIndexV1File = {
|
||||
0, 0, 0, 1, // version
|
||||
|
|
@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
cacheDir =
|
||||
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@After
|
||||
public void tearDown() {
|
||||
Util.recursiveDelete(cacheDir);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddGetRemove() throws Exception {
|
||||
final String key1 = "key1";
|
||||
final String key2 = "key2";
|
||||
|
|
@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(cacheSpanFile.exists()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoad() throws Exception {
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadV1() throws Exception {
|
||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||
fos.write(testIndexV1File);
|
||||
|
|
@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadV2() throws Exception {
|
||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||
fos.write(testIndexV2File);
|
||||
|
|
@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||
}
|
||||
|
||||
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
|
||||
@Test
|
||||
public void testAssignIdForKeyAndGetKeyForId() {
|
||||
final String key1 = "key1";
|
||||
final String key2 = "key2";
|
||||
int id1 = index.assignIdForKey(key1);
|
||||
|
|
@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
|
||||
}
|
||||
|
||||
public void testGetNewId() throws Exception {
|
||||
@Test
|
||||
public void testGetNewId() {
|
||||
SparseArray<String> idToKey = new SparseArray<>();
|
||||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
|
||||
idToKey.put(10, "");
|
||||
|
|
@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryption() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".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));
|
||||
}
|
||||
|
||||
public void testRemoveEmptyNotLockedCachedContent() throws Exception {
|
||||
@Test
|
||||
public void testRemoveEmptyNotLockedCachedContent() {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
|
||||
index.maybeRemove(cachedContent.key);
|
||||
|
|
@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(index.get(cachedContent.key)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCantRemoveNotEmptyCachedContent() throws Exception {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
File cacheSpanFile =
|
||||
|
|
@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertThat(index.get(cachedContent.key)).isNotNull();
|
||||
}
|
||||
|
||||
public void testCantRemoveLockedCachedContent() throws Exception {
|
||||
@Test
|
||||
public void testCantRemoveLockedCachedContent() {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
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.assertWithMessage;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
|
@ -26,11 +27,14 @@ import java.io.IOException;
|
|||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
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}.
|
||||
*/
|
||||
public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link SimpleCacheSpan}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SimpleCacheSpanTest {
|
||||
|
||||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
|
|
@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
|||
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
cacheDir =
|
||||
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@After
|
||||
public void tearDown() {
|
||||
Util.recursiveDelete(cacheDir);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheFile() throws Exception {
|
||||
assertCacheSpan("key1", 0, 0);
|
||||
assertCacheSpan("key2", 1, 2);
|
||||
|
|
@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
|||
+ "A paragraph-separator character \u2029", 1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpgradeFileName() throws Exception {
|
||||
String key = "asd\u00aa";
|
||||
int id = index.assignIdForKey(key);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import android.media.MediaFormat;
|
|||
import android.support.annotation.IntDef;
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.audio.AuxEffectInfo;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -77,6 +78,12 @@ public final class C {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -136,6 +143,8 @@ public final class C {
|
|||
ENCODING_PCM_24BIT,
|
||||
ENCODING_PCM_32BIT,
|
||||
ENCODING_PCM_FLOAT,
|
||||
ENCODING_PCM_MU_LAW,
|
||||
ENCODING_PCM_A_LAW,
|
||||
ENCODING_AC3,
|
||||
ENCODING_E_AC3,
|
||||
ENCODING_DTS,
|
||||
|
|
@ -144,12 +153,19 @@ public final class C {
|
|||
})
|
||||
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)
|
||||
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
|
||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
|
||||
@IntDef({
|
||||
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 {}
|
||||
/** @see 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;
|
||||
/** @see 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 */
|
||||
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
||||
/** @see AudioFormat#ENCODING_E_AC3 */
|
||||
|
|
@ -174,13 +194,6 @@ public final class C {
|
|||
/** @see 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}.
|
||||
*/
|
||||
|
|
@ -271,24 +284,32 @@ public final class C {
|
|||
public static final int 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)
|
||||
@IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
|
||||
USAGE_ASSISTANCE_SONIFICATION, 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})
|
||||
@IntDef({
|
||||
USAGE_ALARM,
|
||||
USAGE_ASSISTANCE_ACCESSIBILITY,
|
||||
USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
|
||||
USAGE_ASSISTANCE_SONIFICATION,
|
||||
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 {}
|
||||
/**
|
||||
* @see 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 =
|
||||
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
|
||||
/**
|
||||
|
|
@ -301,6 +322,8 @@ public final class C {
|
|||
*/
|
||||
public static final int 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
|
||||
*/
|
||||
|
|
@ -353,6 +376,29 @@ public final class C {
|
|||
public static final int 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.
|
||||
*/
|
||||
|
|
@ -368,14 +414,10 @@ public final class C {
|
|||
* 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;
|
||||
/**
|
||||
* Indicates that a buffer is (at least partially) encrypted.
|
||||
*/
|
||||
public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
|
||||
/**
|
||||
* Indicates that a buffer should be decoded but not rendered.
|
||||
*/
|
||||
public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
|
||||
/** 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_DECODE_ONLY = 1 << 31; // 0x80000000
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final int SELECTION_FLAG_DEFAULT = 1;
|
||||
/**
|
||||
* Indicates that the track must be displayed. Only applies to text tracks.
|
||||
*/
|
||||
public static final int SELECTION_FLAG_FORCED = 2;
|
||||
/** Indicates that the track must be displayed. Only applies to text tracks. */
|
||||
public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2
|
||||
/**
|
||||
* Indicates that the player may choose to play the track in absence of an explicit user
|
||||
* 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.
|
||||
|
|
@ -469,32 +509,24 @@ public final class C {
|
|||
*/
|
||||
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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* A data type constant for a manifest file.
|
||||
*/
|
||||
/** A data type constant for a manifest file. */
|
||||
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;
|
||||
/** A data type constant for ads loader data. */
|
||||
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
|
||||
* equal to this value.
|
||||
|
|
@ -694,6 +726,13 @@ public final class C {
|
|||
*/
|
||||
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
|
||||
* {@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;
|
||||
|
||||
/** 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
|
||||
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
|
||||
|
|
|
|||
|
|
@ -89,12 +89,13 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
|||
* model">
|
||||
*
|
||||
* <ul>
|
||||
* <li>It is strongly recommended that ExoPlayer instances are created and accessed from a single
|
||||
* application thread. The application's main thread is ideal. Accessing an instance from
|
||||
* multiple threads is discouraged as it may cause synchronization problems.
|
||||
* <li>Registered listeners are called on the thread that created the ExoPlayer instance, unless
|
||||
* the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
|
||||
* case, registered listeners will be called on the application's main thread.
|
||||
* <li>ExoPlayer instances must be accessed from the thread associated with {@link
|
||||
* #getApplicationLooper()}. This Looper can be specified when creating the player, or this is
|
||||
* the Looper of the thread the player is created on, or the Looper of the application's main
|
||||
* thread if the player is created on a thread without Looper.
|
||||
* <li>Registered listeners are called on the thread associated with {@link
|
||||
* #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
|
||||
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
|
||||
* thread.
|
||||
|
|
@ -178,13 +179,15 @@ public interface ExoPlayer extends Player {
|
|||
@Deprecated
|
||||
@RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
|
||||
|
||||
/**
|
||||
* Gets the {@link Looper} associated with the playback thread.
|
||||
*
|
||||
* @return The {@link Looper} associated with the playback thread.
|
||||
*/
|
||||
/** Returns the {@link Looper} associated with the playback thread. */
|
||||
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
|
||||
* {@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.
|
||||
*/
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
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.FrameworkMediaCrypto;
|
||||
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.Util;
|
||||
|
||||
/**
|
||||
* A factory for {@link ExoPlayer} instances.
|
||||
*/
|
||||
public final class ExoPlayerFactory {
|
||||
|
||||
private static @Nullable BandwidthMeter singletonBandwidthMeter;
|
||||
|
||||
private ExoPlayerFactory() {}
|
||||
|
||||
/**
|
||||
|
|
@ -36,13 +43,14 @@ public final class ExoPlayerFactory {
|
|||
* @param context A {@link Context}.
|
||||
* @param trackSelector The {@link TrackSelector} 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
|
||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
||||
LoadControl loadControl) {
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context, TrackSelector trackSelector, LoadControl loadControl) {
|
||||
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 drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
||||
* will not be used for DRM protected playbacks.
|
||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
||||
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||
* LoadControl)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||
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
|
||||
* extension renderers are used. Note that extensions must be included in the application
|
||||
* build for them to be considered available.
|
||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
||||
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||
* LoadControl)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
||||
LoadControl loadControl, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
@DefaultRenderersFactory.ExtensionRendererMode int 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.
|
||||
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
|
||||
* seamlessly join an ongoing playback.
|
||||
* @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
|
||||
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
|
||||
* LoadControl)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
|
||||
LoadControl loadControl, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
||||
long allowedVideoJoiningTimeMs) {
|
||||
RenderersFactory renderersFactory =
|
||||
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.
|
||||
*/
|
||||
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 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,
|
||||
TrackSelector trackSelector) {
|
||||
return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
|
||||
@Deprecated
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
RenderersFactory renderersFactory, TrackSelector trackSelector) {
|
||||
return newSimpleInstance(
|
||||
/* context= */ null, 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 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 trackSelector The {@link TrackSelector} 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.
|
||||
*/
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||
return newSimpleInstance(
|
||||
renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
|
||||
context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector, LoadControl loadControl) {
|
||||
return new SimpleExoPlayer(
|
||||
renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null);
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl) {
|
||||
return newSimpleInstance(
|
||||
context,
|
||||
renderersFactory,
|
||||
trackSelector,
|
||||
loadControl,
|
||||
/* drmSessionManager= */ null,
|
||||
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.
|
||||
|
|
@ -169,16 +221,48 @@ public final class ExoPlayerFactory {
|
|||
* will not be used for DRM protected playbacks.
|
||||
*/
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
@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.
|
||||
*
|
||||
* @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 trackSelector The {@link TrackSelector} 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.
|
||||
*/
|
||||
public static SimpleExoPlayer newSimpleInstance(
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
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(
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||