Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2

This commit is contained in:
Sebastian Roth 2018-08-23 18:36:02 +08:00
commit e34234b537
No known key found for this signature in database
GPG key ID: 85A9F39F2F7877D4
527 changed files with 33791 additions and 8410 deletions

495
.idea/codeStyleSettings.xml Normal file
View file

@ -0,0 +1,495 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
<option name="USE_RELATIVE_INDENTS" value="false" />
</value>
</option>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="JD_P_AT_EMPTY_LINES" value="false" />
<option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
<option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
<option name="JD_KEEP_EMPTY_RETURN" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<Objective-C>
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
<option name="INDENT_CLASS_MEMBERS" value="2" />
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
</Objective-C>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cc" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:.*Style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_weight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_margin</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:padding</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>

View file

@ -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`.

View file

@ -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

View file

@ -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

View file

@ -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')
}

View file

@ -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'

View file

@ -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:

View file

@ -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'

View file

@ -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);
}

View file

@ -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'

View file

@ -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>

View file

@ -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"
}
]
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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);

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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'
}

View file

@ -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() {

View file

@ -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

View file

@ -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() {}

View file

@ -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 ##

View file

@ -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'

View file

@ -1 +0,0 @@
Copy folders containing architecture specific .so files here.

View file

@ -1 +0,0 @@
Copy cronet.jar and cronet_api.jar here.

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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 \

View file

@ -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 {

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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);
}
}

View 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"/>

View file

@ -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);
}
}

View file

@ -0,0 +1 @@
manifest=src/test/AndroidManifest.xml

View file

@ -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

View file

@ -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;
}

View file

@ -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'
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View 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" />

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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.
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
manifest=src/test/AndroidManifest.xml

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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);
}
}
}
}

View file

@ -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.

View file

@ -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.

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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();

View file

@ -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;
}

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
/**

View file

@ -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)

View file

@ -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; }

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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.

View file

@ -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();
}

View file

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show more