Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
1a5b304b
authored
Jun 24, 2020
by
Oliver Woodman
Committed by
GitHub
Jun 24, 2020
Browse files
Options
_('Browse Files')
Download
Plain Diff
Merge pull request #7517 from google/dev-v2-r2.11.6
r2.11.6
parents
6026b919
67c99e1d
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1079 additions
and
460 deletions
RELEASENOTES.md
constants.gradle
demos/main/src/main/assets/media.exolist.json
extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java
extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
RELEASENOTES.md
View file @
1a5b304b
# Release notes #
### 2.11.6 (2020-06-24) ###
*
UI: Prevent
`PlayerView`
from temporarily hiding the video surface when
seeking to an unprepared period within the current window. For example when
seeking over an ad group, or to the next period in a multi-period DASH
stream (
[
#5507
](
https://github.com/google/ExoPlayer/issues/5507
)
).
*
IMA extension:
*
Add option to skip ads before the start position.
*
Catch unexpected errors in
`stopAd`
to avoid a crash
(
[
#7492
](
https://github.com/google/ExoPlayer/issues/7492
)
).
*
Fix a bug that caused playback to be stuck buffering on resuming from
the background after all ads had played to the end
(
[
#7508
](
https://github.com/google/ExoPlayer/issues/7508
)
).
*
Fix a bug where the number of ads in an ad group couldn't change
(
[
#7477
](
https://github.com/google/ExoPlayer/issues/7477
)
).
*
Work around unexpected
`pauseAd`
/
`stopAd`
for ads that have preloaded
on seeking to another position
(
[
#7492
](
https://github.com/google/ExoPlayer/issues/7492
)
).
*
Fix incorrect rounding of ad cue points.
*
Fix handling of postrolls preloading
(
[
#7518
](
https://github.com/google/ExoPlayer/issues/7518
)
).
### 2.11.5 (2020-06-05) ###
*
Improve the smoothness of video playback immediately after starting, seeking
...
...
@@ -16,10 +38,10 @@
(
[
#7306
](
https://github.com/google/ExoPlayer/issues/7306
)
).
*
Fix issue in
`AudioTrackPositionTracker`
that could cause negative positions
to be reported at the start of playback and immediately after seeking
(
[
#7456
](
https://github.com/google/ExoPlayer/issues/7456
)
.
(
[
#7456
](
https://github.com/google/ExoPlayer/issues/7456
)
)
.
*
Fix further cases where downloads would sometimes not resume after their
network requirements are met
(
[
#7453
](
https://github.com/google/ExoPlayer/issues/7453
)
.
(
[
#7453
](
https://github.com/google/ExoPlayer/issues/7453
)
)
.
*
DASH:
*
Merge trick play adaptation sets (i.e., adaptation sets marked with
`http://dashif.org/guidelines/trickmode`
) into the same
`TrackGroup`
as
...
...
@@ -99,10 +121,10 @@
`DefaultAudioSink`
constructor
(
[
#7134
](
https://github.com/google/ExoPlayer/issues/7134
)
).
*
Workaround issue that could cause slower than realtime playback of AAC on
Android 10 (
[
#6671
](
https://github.com/google/ExoPlayer/issues/6671
)
.
Android 10 (
[
#6671
](
https://github.com/google/ExoPlayer/issues/6671
)
)
.
*
Fix case where another app spuriously holding transient audio focus could
prevent ExoPlayer from acquiring audio focus for an indefinite period of
time (
[
#7182
](
https://github.com/google/ExoPlayer/issues/7182
)
.
time (
[
#7182
](
https://github.com/google/ExoPlayer/issues/7182
)
)
.
*
Fix case where the player volume could be permanently ducked if audio focus
was released whilst ducking.
*
Fix playback of WAV files with trailing non-media bytes
...
...
@@ -1022,7 +1044,7 @@
(
[
#4492
](
https://github.com/google/ExoPlayer/issues/4492
)
and
[
#4634
](
https://github.com/google/ExoPlayer/issues/4634
)
).
*
Fix issue where removing looping media from a playlist throws an exception
(
[
#4871
](
https://github.com/google/ExoPlayer/issues/4871
)
.
(
[
#4871
](
https://github.com/google/ExoPlayer/issues/4871
)
)
.
*
Fix issue where the preferred audio or text track would not be selected if
mapped onto a secondary renderer of the corresponding type
(
[
#4711
](
http://github.com/google/ExoPlayer/issues/4711
)
).
...
...
@@ -1439,7 +1461,7 @@
resources when the playback thread has quit by the time the loading task has
completed.
*
ID3: Better handle malformed ID3 data
(
[
#3792
](
https://github.com/google/ExoPlayer/issues/3792
)
.
(
[
#3792
](
https://github.com/google/ExoPlayer/issues/3792
)
)
.
*
Support 14-bit mode and little endianness in DTS PES packets
(
[
#3340
](
https://github.com/google/ExoPlayer/issues/3340
)
).
*
Demo app: Add ability to download not DRM protected content.
...
...
constants.gradle
View file @
1a5b304b
...
...
@@ -13,8 +13,8 @@
// limitations under the License.
project
.
ext
{
// ExoPlayer version and version code.
releaseVersion
=
'2.11.
5
'
releaseVersionCode
=
201100
5
releaseVersion
=
'2.11.
6
'
releaseVersionCode
=
201100
6
minSdkVersion
=
16
appTargetSdkVersion
=
29
targetSdkVersion
=
28
// TODO: Bump once b/143232359 is resolved
...
...
demos/main/src/main/assets/media.exolist.json
View file @
1a5b304b
...
...
@@ -476,6 +476,11 @@
"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"
:
"https://vastsynthesizer.appspot.com/empty-midroll-2"
},
{
"name"
:
"VMAP midroll at 1765 s"
,
"uri"
:
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
,
"ad_tag_uri"
:
"https://vastsynthesizer.appspot.com/midroll-large"
}
]
},
...
...
extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java
0 → 100644
View file @
1a5b304b
/*
* Copyright (C) 2020 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.source.ads.AdPlaybackState
;
import
java.util.Arrays
;
import
java.util.List
;
/**
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
*/
/* package */
final
class
AdPlaybackStateFactory
{
private
AdPlaybackStateFactory
()
{}
/**
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public
static
AdPlaybackState
fromCuePoints
(
List
<
Float
>
cuePoints
)
{
if
(
cuePoints
.
isEmpty
())
{
// If no cue points are specified, there is a preroll ad.
return
new
AdPlaybackState
(
/* adGroupTimesUs...= */
0
);
}
int
count
=
cuePoints
.
size
();
long
[]
adGroupTimesUs
=
new
long
[
count
];
int
adGroupIndex
=
0
;
for
(
int
i
=
0
;
i
<
count
;
i
++)
{
double
cuePoint
=
cuePoints
.
get
(
i
);
if
(
cuePoint
==
-
1.0
)
{
adGroupTimesUs
[
count
-
1
]
=
C
.
TIME_END_OF_SOURCE
;
}
else
{
adGroupTimesUs
[
adGroupIndex
++]
=
Math
.
round
(
C
.
MICROS_PER_SECOND
*
cuePoint
);
}
}
// Cue points may be out of order, so sort them.
Arrays
.
sort
(
adGroupTimesUs
,
0
,
adGroupIndex
);
return
new
AdPlaybackState
(
adGroupTimesUs
);
}
}
extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
View file @
1a5b304b
...
...
@@ -86,14 +86,7 @@ import java.util.Set;
* For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link
* AdViewProvider#getAdOverlayViews()}.
*/
public
final
class
ImaAdsLoader
implements
Player
.
EventListener
,
AdsLoader
,
VideoAdPlayer
,
ContentProgressProvider
,
AdErrorListener
,
AdsLoadedListener
,
AdEventListener
{
public
final
class
ImaAdsLoader
implements
Player
.
EventListener
,
AdsLoader
{
static
{
ExoPlayerLibraryInfo
.
registerModule
(
"goog.exo.ima"
);
...
...
@@ -123,6 +116,7 @@ public final class ImaAdsLoader
private
int
mediaLoadTimeoutMs
;
private
int
mediaBitrate
;
private
boolean
focusSkipButtonWhenAvailable
;
private
boolean
playAdBeforeStartPosition
;
private
ImaFactory
imaFactory
;
/**
...
...
@@ -137,6 +131,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs
=
TIMEOUT_UNSET
;
mediaBitrate
=
BITRATE_UNSET
;
focusSkipButtonWhenAvailable
=
true
;
playAdBeforeStartPosition
=
true
;
imaFactory
=
new
DefaultImaFactory
();
}
...
...
@@ -250,6 +245,21 @@ public final class ImaAdsLoader
return
this
;
}
/**
* Sets whether to play an ad before the start position when beginning playback. If {@code
* true}, an ad will be played if there is one at or before the start position. If {@code
* false}, an ad will be played only if there is one exactly at the start position. The default
* setting is {@code true}.
*
* @param playAdBeforeStartPosition Whether to play an ad before the start position when
* beginning playback.
* @return This builder, for convenience.
*/
public
Builder
setPlayAdBeforeStartPosition
(
boolean
playAdBeforeStartPosition
)
{
this
.
playAdBeforeStartPosition
=
playAdBeforeStartPosition
;
return
this
;
}
@VisibleForTesting
/* package */
Builder
setImaFactory
(
ImaFactory
imaFactory
)
{
this
.
imaFactory
=
Assertions
.
checkNotNull
(
imaFactory
);
...
...
@@ -275,6 +285,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs
,
mediaBitrate
,
focusSkipButtonWhenAvailable
,
playAdBeforeStartPosition
,
adUiElements
,
adEventListener
,
imaFactory
);
...
...
@@ -298,6 +309,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs
,
mediaBitrate
,
focusSkipButtonWhenAvailable
,
playAdBeforeStartPosition
,
adUiElements
,
adEventListener
,
imaFactory
);
...
...
@@ -331,6 +343,8 @@ public final class ImaAdsLoader
* milliseconds.
*/
private
static
final
long
THRESHOLD_AD_PRELOAD_MS
=
4000
;
/** The threshold below which ad cue points are treated as matching, in microseconds. */
private
static
final
long
THRESHOLD_AD_MATCH_US
=
1000
;
private
static
final
int
TIMEOUT_UNSET
=
-
1
;
private
static
final
int
BITRATE_UNSET
=
-
1
;
...
...
@@ -345,12 +359,13 @@ public final class ImaAdsLoader
*/
private
static
final
int
IMA_AD_STATE_NONE
=
0
;
/**
* The ad playback state when IMA has called {@link
#playAd(AdMediaInfo)} and not {@link
* #pauseAd(AdMediaInfo)}.
* The ad playback state when IMA has called {@link
ComponentListener#playAd(AdMediaInfo)} and not
*
{@link ComponentListener#
#pauseAd(AdMediaInfo)}.
*/
private
static
final
int
IMA_AD_STATE_PLAYING
=
1
;
/**
* The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad.
* The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while
* playing an ad.
*/
private
static
final
int
IMA_AD_STATE_PAUSED
=
2
;
...
...
@@ -360,13 +375,15 @@ public final class ImaAdsLoader
private
final
int
vastLoadTimeoutMs
;
private
final
int
mediaLoadTimeoutMs
;
private
final
boolean
focusSkipButtonWhenAvailable
;
private
final
boolean
playAdBeforeStartPosition
;
private
final
int
mediaBitrate
;
@Nullable
private
final
Set
<
UiElement
>
adUiElements
;
@Nullable
private
final
AdEventListener
adEventListener
;
private
final
ImaFactory
imaFactory
;
private
final
Timeline
.
Period
period
;
private
final
Handler
handler
;
private
final
List
<
VideoAdPlayerCallback
>
adCallbacks
;
private
final
ComponentListener
componentListener
;
private
final
List
<
VideoAdPlayer
.
VideoAdPlayerCallback
>
adCallbacks
;
private
final
AdDisplayContainer
adDisplayContainer
;
private
final
com
.
google
.
ads
.
interactivemedia
.
v3
.
api
.
AdsLoader
adsLoader
;
private
final
Runnable
updateAdProgressRunnable
;
...
...
@@ -380,10 +397,10 @@ public final class ImaAdsLoader
@Nullable
private
Player
player
;
private
VideoProgressUpdate
lastContentProgress
;
private
VideoProgressUpdate
lastAdProgress
;
private
int
lastVolumePercent
age
;
private
int
lastVolumePercent
;
@Nullable
private
AdsManager
adsManager
;
private
boolean
i
nitializedAdsManager
;
private
boolean
i
sAdsManagerInitialized
;
private
boolean
hasAdPlaybackState
;
@Nullable
private
AdLoadException
pendingAdLoadError
;
private
Timeline
timeline
;
...
...
@@ -423,10 +440,10 @@ public final class ImaAdsLoader
*/
@Nullable
private
AdInfo
pendingAdPrepareErrorAdInfo
;
/**
* If a content period has finished but IMA has not yet called {@link
#playAd(AdMediaInfo)},
*
stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing.
*
This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET}
* otherwise.
* If a content period has finished but IMA has not yet called {@link
*
ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link
*
SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine
*
a fake, increasing content position. {@link C#TIME_UNSET}
otherwise.
*/
private
long
fakeContentProgressElapsedRealtimeMs
;
/**
...
...
@@ -436,7 +453,10 @@ public final class ImaAdsLoader
private
long
fakeContentProgressOffsetMs
;
/** 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 ComponentListener#getContentProgress()} has sent {@link
* #pendingContentPositionMs} to IMA.
*/
private
boolean
sentPendingContentPositionMs
;
/**
* Stores the real time in milliseconds at which the player started buffering, possibly due to not
...
...
@@ -465,6 +485,7 @@ public final class ImaAdsLoader
/* mediaLoadTimeoutMs= */
TIMEOUT_UNSET
,
/* mediaBitrate= */
BITRATE_UNSET
,
/* focusSkipButtonWhenAvailable= */
true
,
/* playAdBeforeStartPosition= */
true
,
/* adUiElements= */
null
,
/* adEventListener= */
null
,
/* imaFactory= */
new
DefaultImaFactory
());
...
...
@@ -481,6 +502,7 @@ public final class ImaAdsLoader
int
mediaLoadTimeoutMs
,
int
mediaBitrate
,
boolean
focusSkipButtonWhenAvailable
,
boolean
playAdBeforeStartPosition
,
@Nullable
Set
<
UiElement
>
adUiElements
,
@Nullable
AdEventListener
adEventListener
,
ImaFactory
imaFactory
)
{
...
...
@@ -492,6 +514,7 @@ public final class ImaAdsLoader
this
.
mediaLoadTimeoutMs
=
mediaLoadTimeoutMs
;
this
.
mediaBitrate
=
mediaBitrate
;
this
.
focusSkipButtonWhenAvailable
=
focusSkipButtonWhenAvailable
;
this
.
playAdBeforeStartPosition
=
playAdBeforeStartPosition
;
this
.
adUiElements
=
adUiElements
;
this
.
adEventListener
=
adEventListener
;
this
.
imaFactory
=
imaFactory
;
...
...
@@ -505,14 +528,15 @@ public final class ImaAdsLoader
imaSdkSettings
.
setPlayerVersion
(
IMA_SDK_SETTINGS_PLAYER_VERSION
);
period
=
new
Timeline
.
Period
();
handler
=
Util
.
createHandler
(
getImaLooper
(),
/* callback= */
null
);
componentListener
=
new
ComponentListener
();
adCallbacks
=
new
ArrayList
<>(
/* initialCapacity= */
1
);
adDisplayContainer
=
imaFactory
.
createAdDisplayContainer
();
adDisplayContainer
.
setPlayer
(
/* videoAdPlayer= */
this
);
adDisplayContainer
.
setPlayer
(
/* videoAdPlayer= */
componentListener
);
adsLoader
=
imaFactory
.
createAdsLoader
(
context
.
getApplicationContext
(),
imaSdkSettings
,
adDisplayContainer
);
adsLoader
.
addAdErrorListener
(
/* adErrorListener= */
this
);
adsLoader
.
addAdsLoadedListener
(
/* adsLoadedListener= */
this
);
adsLoader
.
addAdErrorListener
(
componentListener
);
adsLoader
.
addAdsLoadedListener
(
componentListener
);
updateAdProgressRunnable
=
this
::
updateAdProgress
;
adInfoByAdMediaInfo
=
new
HashMap
<>();
supportedMimeTypes
=
Collections
.
emptyList
();
...
...
@@ -573,7 +597,7 @@ public final class ImaAdsLoader
if
(
vastLoadTimeoutMs
!=
TIMEOUT_UNSET
)
{
request
.
setVastLoadTimeout
(
vastLoadTimeoutMs
);
}
request
.
setContentProgressProvider
(
this
);
request
.
setContentProgressProvider
(
componentListener
);
pendingAdRequestContext
=
new
Object
();
request
.
setUserRequestContext
(
pendingAdRequestContext
);
adsLoader
.
requestAds
(
request
);
...
...
@@ -622,7 +646,7 @@ public final class ImaAdsLoader
player
.
addListener
(
this
);
boolean
playWhenReady
=
player
.
getPlayWhenReady
();
this
.
eventListener
=
eventListener
;
lastVolumePercent
age
=
0
;
lastVolumePercent
=
0
;
lastAdProgress
=
VideoProgressUpdate
.
VIDEO_TIME_NOT_READY
;
lastContentProgress
=
VideoProgressUpdate
.
VIDEO_TIME_NOT_READY
;
ViewGroup
adViewGroup
=
adViewProvider
.
getAdViewGroup
();
...
...
@@ -639,7 +663,7 @@ public final class ImaAdsLoader
adsManager
.
resume
();
}
}
else
if
(
adsManager
!=
null
)
{
adPlaybackState
=
new
AdPlaybackState
(
getAdGroupTimesUs
(
adsManager
.
getAdCuePoints
()
));
adPlaybackState
=
AdPlaybackStateFactory
.
fromCuePoints
(
adsManager
.
getAdCuePoints
(
));
updateAdPlaybackState
();
}
else
{
// Ads haven't loaded yet, so request them.
...
...
@@ -659,9 +683,9 @@ public final class ImaAdsLoader
adPlaybackState
.
withAdResumePositionUs
(
playingAd
?
C
.
msToUs
(
player
.
getCurrentPosition
())
:
0
);
}
lastVolumePercent
age
=
getVolume
();
lastVolumePercent
=
getPlayerVolumePercent
();
lastAdProgress
=
getAdVideoProgressUpdate
();
lastContentProgress
=
getContent
Progress
();
lastContentProgress
=
getContent
VideoProgressUpdate
();
adDisplayContainer
.
unregisterAllVideoControlsOverlays
();
player
.
removeListener
(
this
);
this
.
player
=
null
;
...
...
@@ -671,17 +695,9 @@ public final class ImaAdsLoader
@Override
public
void
release
()
{
pendingAdRequestContext
=
null
;
if
(
adsManager
!=
null
)
{
adsManager
.
removeAdErrorListener
(
this
);
adsManager
.
removeAdEventListener
(
this
);
if
(
adEventListener
!=
null
)
{
adsManager
.
removeAdEventListener
(
adEventListener
);
}
adsManager
.
destroy
();
adsManager
=
null
;
}
adsLoader
.
removeAdsLoadedListener
(
/* adsLoadedListener= */
this
);
adsLoader
.
removeAdErrorListener
(
/* adErrorListener= */
this
);
destroyAdsManager
();
adsLoader
.
removeAdsLoadedListener
(
componentListener
);
adsLoader
.
removeAdErrorListener
(
componentListener
);
imaPausedContent
=
false
;
imaAdState
=
IMA_AD_STATE_NONE
;
imaAdMediaInfo
=
null
;
...
...
@@ -689,7 +705,7 @@ public final class ImaAdsLoader
imaAdInfo
=
null
;
pendingAdLoadError
=
null
;
adPlaybackState
=
AdPlaybackState
.
NONE
;
hasAdPlaybackState
=
fals
e
;
hasAdPlaybackState
=
tru
e
;
updateAdPlaybackState
();
}
...
...
@@ -700,271 +716,11 @@ public final class ImaAdsLoader
}
try
{
handleAdPrepareError
(
adGroupIndex
,
adIndexInAdGroup
,
exception
);
}
catch
(
Exception
e
)
{
}
catch
(
Runtime
Exception
e
)
{
maybeNotifyInternalError
(
"handlePrepareError"
,
e
);
}
}
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public
void
onAdsManagerLoaded
(
AdsManagerLoadedEvent
adsManagerLoadedEvent
)
{
AdsManager
adsManager
=
adsManagerLoadedEvent
.
getAdsManager
();
if
(!
Util
.
areEqual
(
pendingAdRequestContext
,
adsManagerLoadedEvent
.
getUserRequestContext
()))
{
adsManager
.
destroy
();
return
;
}
pendingAdRequestContext
=
null
;
this
.
adsManager
=
adsManager
;
adsManager
.
addAdErrorListener
(
this
);
adsManager
.
addAdEventListener
(
this
);
if
(
adEventListener
!=
null
)
{
adsManager
.
addAdEventListener
(
adEventListener
);
}
if
(
player
!=
null
)
{
// If a player is attached already, start playback immediately.
try
{
adPlaybackState
=
new
AdPlaybackState
(
getAdGroupTimesUs
(
adsManager
.
getAdCuePoints
()));
hasAdPlaybackState
=
true
;
updateAdPlaybackState
();
}
catch
(
Exception
e
)
{
maybeNotifyInternalError
(
"onAdsManagerLoaded"
,
e
);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public
void
onAdEvent
(
AdEvent
adEvent
)
{
AdEventType
adEventType
=
adEvent
.
getType
();
if
(
DEBUG
&&
adEventType
!=
AdEventType
.
AD_PROGRESS
)
{
Log
.
d
(
TAG
,
"onAdEvent: "
+
adEventType
);
}
if
(
adsManager
==
null
)
{
// Drop events after release.
return
;
}
try
{
handleAdEvent
(
adEvent
);
}
catch
(
Exception
e
)
{
maybeNotifyInternalError
(
"onAdEvent"
,
e
);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public
void
onAdError
(
AdErrorEvent
adErrorEvent
)
{
AdError
error
=
adErrorEvent
.
getError
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"onAdError"
,
error
);
}
if
(
adsManager
==
null
)
{
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext
=
null
;
adPlaybackState
=
AdPlaybackState
.
NONE
;
hasAdPlaybackState
=
true
;
updateAdPlaybackState
();
}
else
if
(
isAdGroupLoadError
(
error
))
{
try
{
handleAdGroupLoadError
(
error
);
}
catch
(
Exception
e
)
{
maybeNotifyInternalError
(
"onAdError"
,
e
);
}
}
if
(
pendingAdLoadError
==
null
)
{
pendingAdLoadError
=
AdLoadException
.
createForAllAds
(
error
);
}
maybeNotifyPendingAdLoadError
();
}
// ContentProgressProvider implementation.
@Override
public
VideoProgressUpdate
getContentProgress
()
{
VideoProgressUpdate
videoProgressUpdate
=
getContentVideoProgressUpdate
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Content progress: "
+
videoProgressUpdate
);
}
if
(
waitingForPreloadElapsedRealtimeMs
!=
C
.
TIME_UNSET
)
{
// IMA is polling the player position but we are buffering for an ad to preload, so playback
// may be stuck. Detect this case and signal an error if applicable.
long
stuckElapsedRealtimeMs
=
SystemClock
.
elapsedRealtime
()
-
waitingForPreloadElapsedRealtimeMs
;
if
(
stuckElapsedRealtimeMs
>=
THRESHOLD_AD_PRELOAD_MS
)
{
waitingForPreloadElapsedRealtimeMs
=
C
.
TIME_UNSET
;
handleAdGroupLoadError
(
new
IOException
(
"Ad preloading timed out"
));
maybeNotifyPendingAdLoadError
();
}
}
return
videoProgressUpdate
;
}
// VideoAdPlayer implementation.
@Override
public
VideoProgressUpdate
getAdProgress
()
{
throw
new
IllegalStateException
(
"Unexpected call to getAdProgress when using preloading"
);
}
@Override
public
int
getVolume
()
{
@Nullable
Player
player
=
this
.
player
;
if
(
player
==
null
)
{
return
lastVolumePercentage
;
}
@Nullable
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
(
AdMediaInfo
adMediaInfo
,
AdPodInfo
adPodInfo
)
{
try
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"loadAd "
+
getAdMediaInfoString
(
adMediaInfo
)
+
", ad pod "
+
adPodInfo
);
}
if
(
adsManager
==
null
)
{
// Drop events after release.
return
;
}
int
adGroupIndex
=
getAdGroupIndexForAdPod
(
adPodInfo
);
int
adIndexInAdGroup
=
adPodInfo
.
getAdPosition
()
-
1
;
AdInfo
adInfo
=
new
AdInfo
(
adGroupIndex
,
adIndexInAdGroup
);
adInfoByAdMediaInfo
.
put
(
adMediaInfo
,
adInfo
);
if
(
adPlaybackState
.
isAdInErrorState
(
adGroupIndex
,
adIndexInAdGroup
))
{
// We have already marked this ad as having failed to load, so ignore the request. IMA will
// timeout after its media load timeout.
return
;
}
AdPlaybackState
.
AdGroup
adGroup
=
adPlaybackState
.
adGroups
[
adInfo
.
adGroupIndex
];
if
(
adGroup
.
count
==
C
.
LENGTH_UNSET
)
{
adPlaybackState
=
adPlaybackState
.
withAdCount
(
adInfo
.
adGroupIndex
,
Math
.
max
(
adPodInfo
.
getTotalAds
(),
adGroup
.
states
.
length
));
adGroup
=
adPlaybackState
.
adGroups
[
adInfo
.
adGroupIndex
];
}
for
(
int
i
=
0
;
i
<
adIndexInAdGroup
;
i
++)
{
// Any preceding ads that haven't loaded are not going to load.
if
(
adGroup
.
states
[
i
]
==
AdPlaybackState
.
AD_STATE_UNAVAILABLE
)
{
adPlaybackState
=
adPlaybackState
.
withAdLoadError
(
/* adGroupIndex= */
adGroupIndex
,
/* adIndexInAdGroup= */
i
);
}
}
Uri
adUri
=
Uri
.
parse
(
adMediaInfo
.
getUrl
());
adPlaybackState
=
adPlaybackState
.
withAdUri
(
adInfo
.
adGroupIndex
,
adInfo
.
adIndexInAdGroup
,
adUri
);
updateAdPlaybackState
();
}
catch
(
Exception
e
)
{
maybeNotifyInternalError
(
"loadAd"
,
e
);
}
}
@Override
public
void
addCallback
(
VideoAdPlayerCallback
videoAdPlayerCallback
)
{
adCallbacks
.
add
(
videoAdPlayerCallback
);
}
@Override
public
void
removeCallback
(
VideoAdPlayerCallback
videoAdPlayerCallback
)
{
adCallbacks
.
remove
(
videoAdPlayerCallback
);
}
@Override
public
void
playAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"playAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adsManager
==
null
)
{
// Drop events after release.
return
;
}
if
(
imaAdState
==
IMA_AD_STATE_PLAYING
)
{
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log
.
w
(
TAG
,
"Unexpected playAd without stopAd"
);
}
if
(
imaAdState
==
IMA_AD_STATE_NONE
)
{
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs
=
C
.
TIME_UNSET
;
fakeContentProgressOffsetMs
=
C
.
TIME_UNSET
;
imaAdState
=
IMA_AD_STATE_PLAYING
;
imaAdMediaInfo
=
adMediaInfo
;
imaAdInfo
=
Assertions
.
checkNotNull
(
adInfoByAdMediaInfo
.
get
(
adMediaInfo
));
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onPlay
(
adMediaInfo
);
}
if
(
pendingAdPrepareErrorAdInfo
!=
null
&&
pendingAdPrepareErrorAdInfo
.
equals
(
imaAdInfo
))
{
pendingAdPrepareErrorAdInfo
=
null
;
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onError
(
adMediaInfo
);
}
}
updateAdProgress
();
}
else
{
imaAdState
=
IMA_AD_STATE_PLAYING
;
Assertions
.
checkState
(
adMediaInfo
.
equals
(
imaAdMediaInfo
));
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onResume
(
adMediaInfo
);
}
}
if
(!
Assertions
.
checkNotNull
(
player
).
getPlayWhenReady
())
{
Assertions
.
checkNotNull
(
adsManager
).
pause
();
}
}
@Override
public
void
stopAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"stopAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adsManager
==
null
)
{
// Drop event after release.
return
;
}
Assertions
.
checkNotNull
(
player
);
Assertions
.
checkState
(
imaAdState
!=
IMA_AD_STATE_NONE
);
try
{
stopAdInternal
();
}
catch
(
Exception
e
)
{
maybeNotifyInternalError
(
"stopAd"
,
e
);
}
}
@Override
public
void
pauseAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"pauseAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
imaAdState
==
IMA_AD_STATE_NONE
)
{
// This method is called after content is resumed.
return
;
}
Assertions
.
checkState
(
adMediaInfo
.
equals
(
imaAdMediaInfo
));
imaAdState
=
IMA_AD_STATE_PAUSED
;
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onPause
(
adMediaInfo
);
}
}
// Player.EventListener implementation.
@Override
...
...
@@ -980,9 +736,21 @@ public final class ImaAdsLoader
if
(
contentDurationUs
!=
C
.
TIME_UNSET
)
{
adPlaybackState
=
adPlaybackState
.
withContentDurationUs
(
contentDurationUs
);
}
if
(!
initializedAdsManager
&&
adsManager
!=
null
)
{
initializedAdsManager
=
true
;
initializeAdsManager
(
adsManager
);
@Nullable
AdsManager
adsManager
=
this
.
adsManager
;
if
(!
isAdsManagerInitialized
&&
adsManager
!=
null
)
{
isAdsManagerInitialized
=
true
;
@Nullable
AdsRenderingSettings
adsRenderingSettings
=
setupAdsRendering
();
if
(
adsRenderingSettings
==
null
)
{
// There are no ads to play.
destroyAdsManager
();
}
else
{
adsManager
.
init
(
adsRenderingSettings
);
adsManager
.
start
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Initialized with ads rendering settings: "
+
adsRenderingSettings
);
}
}
updateAdPlaybackState
();
}
handleTimelineOrPositionChanged
();
}
...
...
@@ -1047,7 +815,12 @@ public final class ImaAdsLoader
// Internal methods.
private
void
initializeAdsManager
(
AdsManager
adsManager
)
{
/**
* Configures ads rendering for starting playback, returning the settings for the IMA SDK or
* {@code null} if no ads should play.
*/
@Nullable
private
AdsRenderingSettings
setupAdsRendering
()
{
AdsRenderingSettings
adsRenderingSettings
=
imaFactory
.
createAdsRenderingSettings
();
adsRenderingSettings
.
setEnablePreloading
(
true
);
adsRenderingSettings
.
setMimeTypes
(
supportedMimeTypes
);
...
...
@@ -1063,36 +836,48 @@ public final class ImaAdsLoader
}
// Skip ads based on the start position as required.
long
[]
adGroupTimesUs
=
getAdGroupTimesUs
(
adsManager
.
getAdCuePoints
())
;
long
[]
adGroupTimesUs
=
adPlaybackState
.
adGroupTimesUs
;
long
contentPositionMs
=
getContentPeriodPositionMs
(
Assertions
.
checkNotNull
(
player
),
timeline
,
period
);
int
adGroup
IndexForPosition
=
int
adGroup
ForPositionIndex
=
adPlaybackState
.
getAdGroupIndexForPositionUs
(
C
.
msToUs
(
contentPositionMs
),
C
.
msToUs
(
contentDurationMs
));
if
(
adGroupIndexForPosition
>
0
&&
adGroupIndexForPosition
!=
C
.
INDEX_UNSET
)
{
// Skip any ad groups before the one at or immediately before the playback position.
for
(
int
i
=
0
;
i
<
adGroupIndexForPosition
;
i
++)
{
adPlaybackState
=
adPlaybackState
.
withSkippedAdGroup
(
i
);
if
(
adGroupForPositionIndex
!=
C
.
INDEX_UNSET
)
{
boolean
playAdWhenStartingPlayback
=
playAdBeforeStartPosition
||
adGroupTimesUs
[
adGroupForPositionIndex
]
==
C
.
msToUs
(
contentPositionMs
);
if
(!
playAdWhenStartingPlayback
)
{
adGroupForPositionIndex
++;
}
else
if
(
hasMidrollAdGroups
(
adGroupTimesUs
))
{
// Provide the player's initial position to trigger loading and playing the ad. If there are
// no midrolls, we are playing a preroll and any pending content position wouldn't be
// cleared.
pendingContentPositionMs
=
contentPositionMs
;
}
if
(
adGroupForPositionIndex
>
0
)
{
for
(
int
i
=
0
;
i
<
adGroupForPositionIndex
;
i
++)
{
adPlaybackState
=
adPlaybackState
.
withSkippedAdGroup
(
i
);
}
if
(
adGroupForPositionIndex
==
adGroupTimesUs
.
length
)
{
// We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP
// ads, we signal that no ads will render so the caller can destroy the ads manager.
return
null
;
}
long
adGroupForPositionTimeUs
=
adGroupTimesUs
[
adGroupForPositionIndex
];
long
adGroupBeforePositionTimeUs
=
adGroupTimesUs
[
adGroupForPositionIndex
-
1
];
if
(
adGroupForPositionTimeUs
==
C
.
TIME_END_OF_SOURCE
)
{
// Play the postroll by offsetting the start position just past the last non-postroll ad.
adsRenderingSettings
.
setPlayAdsAfterTime
(
(
double
)
adGroupBeforePositionTimeUs
/
C
.
MICROS_PER_SECOND
+
1
d
);
}
else
{
// Play ads after the midpoint between the ad to play and the one before it, to avoid
// issues with rounding one of the two ad times.
double
midpointTimeUs
=
(
adGroupForPositionTimeUs
+
adGroupBeforePositionTimeUs
)
/
2
d
;
adsRenderingSettings
.
setPlayAdsAfterTime
(
midpointTimeUs
/
C
.
MICROS_PER_SECOND
);
}
}
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times.
long
adGroupForPositionTimeUs
=
adGroupTimesUs
[
adGroupIndexForPosition
];
long
adGroupBeforeTimeUs
=
adGroupTimesUs
[
adGroupIndexForPosition
-
1
];
double
midpointTimeUs
=
(
adGroupForPositionTimeUs
+
adGroupBeforeTimeUs
)
/
2
d
;
adsRenderingSettings
.
setPlayAdsAfterTime
(
midpointTimeUs
/
C
.
MICROS_PER_SECOND
);
}
if
(
adGroupIndexForPosition
!=
C
.
INDEX_UNSET
&&
hasMidrollAdGroups
(
adGroupTimesUs
))
{
// Provide the player's initial position to trigger loading and playing the ad.
pendingContentPositionMs
=
contentPositionMs
;
}
adsManager
.
init
(
adsRenderingSettings
);
adsManager
.
start
();
updateAdPlaybackState
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Initialized with ads rendering settings: "
+
adsRenderingSettings
);
}
return
adsRenderingSettings
;
}
private
void
handleAdEvent
(
AdEvent
adEvent
)
{
...
...
@@ -1203,6 +988,27 @@ public final class ImaAdsLoader
handler
.
removeCallbacks
(
updateAdProgressRunnable
);
}
private
int
getPlayerVolumePercent
()
{
@Nullable
Player
player
=
this
.
player
;
if
(
player
==
null
)
{
return
lastVolumePercent
;
}
@Nullable
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
;
}
private
void
handlePlayerStateChanged
(
boolean
playWhenReady
,
@Player
.
State
int
playbackState
)
{
if
(
playingAd
&&
imaAdState
==
IMA_AD_STATE_PLAYING
)
{
if
(!
bufferingAd
&&
playbackState
==
Player
.
STATE_BUFFERING
)
{
...
...
@@ -1283,11 +1089,19 @@ public final class ImaAdsLoader
}
if
(!
sentContentComplete
&&
!
wasPlayingAd
&&
playingAd
&&
imaAdState
==
IMA_AD_STATE_NONE
)
{
int
adGroupIndex
=
player
.
getCurrentAdGroupIndex
();
// IMA hasn't called playAd yet, so fake the content position.
fakeContentProgressElapsedRealtimeMs
=
SystemClock
.
elapsedRealtime
();
fakeContentProgressOffsetMs
=
C
.
usToMs
(
adPlaybackState
.
adGroupTimesUs
[
adGroupIndex
]);
if
(
fakeContentProgressOffsetMs
==
C
.
TIME_END_OF_SOURCE
)
{
fakeContentProgressOffsetMs
=
contentDurationMs
;
if
(
adPlaybackState
.
adGroupTimesUs
[
adGroupIndex
]
==
C
.
TIME_END_OF_SOURCE
)
{
adsLoader
.
contentComplete
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"adsLoader.contentComplete from period transition"
);
}
sentContentComplete
=
true
;
}
else
{
// IMA hasn't called playAd yet, so fake the content position.
fakeContentProgressElapsedRealtimeMs
=
SystemClock
.
elapsedRealtime
();
fakeContentProgressOffsetMs
=
C
.
usToMs
(
adPlaybackState
.
adGroupTimesUs
[
adGroupIndex
]);
if
(
fakeContentProgressOffsetMs
==
C
.
TIME_END_OF_SOURCE
)
{
fakeContentProgressOffsetMs
=
contentDurationMs
;
}
}
}
}
...
...
@@ -1406,7 +1220,7 @@ public final class ImaAdsLoader
&&
positionMs
+
THRESHOLD_END_OF_CONTENT_MS
>=
contentDurationMs
)
{
adsLoader
.
contentComplete
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"adsLoader.contentComplete"
);
Log
.
d
(
TAG
,
"adsLoader.contentComplete
from content position check
"
);
}
sentContentComplete
=
true
;
}
...
...
@@ -1448,9 +1262,15 @@ public final class ImaAdsLoader
}
// adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead.
long
adGroupTimeUs
=
(
long
)
(((
float
)
adPodInfo
.
getTimeOffset
())
*
C
.
MICROS_PER_SECOND
);
// We receive cue points from IMA SDK as floats. This code replicates the same calculation used
// to populate adGroupTimesUs (having truncated input back to float, to avoid failures if the
// behavior of the IMA SDK changes to provide greater precision in AdPodInfo).
long
adPodTimeUs
=
Math
.
round
((
double
)
((
float
)
adPodInfo
.
getTimeOffset
())
*
C
.
MICROS_PER_SECOND
);
for
(
int
adGroupIndex
=
0
;
adGroupIndex
<
adPlaybackState
.
adGroupCount
;
adGroupIndex
++)
{
if
(
adPlaybackState
.
adGroupTimesUs
[
adGroupIndex
]
==
adGroupTimeUs
)
{
long
adGroupTimeUs
=
adPlaybackState
.
adGroupTimesUs
[
adGroupIndex
];
if
(
adGroupTimeUs
!=
C
.
TIME_END_OF_SOURCE
&&
Math
.
abs
(
adGroupTimeUs
-
adPodTimeUs
)
<
THRESHOLD_AD_MATCH_US
)
{
return
adGroupIndex
;
}
}
...
...
@@ -1492,28 +1312,6 @@ public final class ImaAdsLoader
:
timeline
.
getPeriod
(
/* periodIndex= */
0
,
period
).
getPositionInWindowMs
());
}
private
static
long
[]
getAdGroupTimesUs
(
List
<
Float
>
cuePoints
)
{
if
(
cuePoints
.
isEmpty
())
{
// If no cue points are specified, there is a preroll ad.
return
new
long
[]
{
0
};
}
int
count
=
cuePoints
.
size
();
long
[]
adGroupTimesUs
=
new
long
[
count
];
int
adGroupIndex
=
0
;
for
(
int
i
=
0
;
i
<
count
;
i
++)
{
double
cuePoint
=
cuePoints
.
get
(
i
);
if
(
cuePoint
==
-
1.0
)
{
adGroupTimesUs
[
count
-
1
]
=
C
.
TIME_END_OF_SOURCE
;
}
else
{
adGroupTimesUs
[
adGroupIndex
++]
=
(
long
)
(
C
.
MICROS_PER_SECOND
*
cuePoint
);
}
}
// Cue points may be out of order, so sort them.
Arrays
.
sort
(
adGroupTimesUs
,
0
,
adGroupIndex
);
return
adGroupTimesUs
;
}
private
static
boolean
isAdGroupLoadError
(
AdError
adError
)
{
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
...
...
@@ -1539,6 +1337,18 @@ public final class ImaAdsLoader
}
}
private
void
destroyAdsManager
()
{
if
(
adsManager
!=
null
)
{
adsManager
.
removeAdErrorListener
(
componentListener
);
adsManager
.
removeAdEventListener
(
componentListener
);
if
(
adEventListener
!=
null
)
{
adsManager
.
removeAdEventListener
(
adEventListener
);
}
adsManager
.
destroy
();
adsManager
=
null
;
}
}
/** Factory for objects provided by the IMA SDK. */
@VisibleForTesting
/* package */
interface
ImaFactory
{
...
...
@@ -1555,6 +1365,299 @@ public final class ImaAdsLoader
Context
context
,
ImaSdkSettings
imaSdkSettings
,
AdDisplayContainer
adDisplayContainer
);
}
private
final
class
ComponentListener
implements
VideoAdPlayer
,
ContentProgressProvider
,
AdErrorListener
,
AdsLoadedListener
,
AdEventListener
{
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public
void
onAdsManagerLoaded
(
AdsManagerLoadedEvent
adsManagerLoadedEvent
)
{
AdsManager
adsManager
=
adsManagerLoadedEvent
.
getAdsManager
();
if
(!
Util
.
areEqual
(
pendingAdRequestContext
,
adsManagerLoadedEvent
.
getUserRequestContext
()))
{
adsManager
.
destroy
();
return
;
}
pendingAdRequestContext
=
null
;
ImaAdsLoader
.
this
.
adsManager
=
adsManager
;
adsManager
.
addAdErrorListener
(
this
);
adsManager
.
addAdEventListener
(
this
);
if
(
adEventListener
!=
null
)
{
adsManager
.
addAdEventListener
(
adEventListener
);
}
if
(
player
!=
null
)
{
// If a player is attached already, start playback immediately.
try
{
adPlaybackState
=
AdPlaybackStateFactory
.
fromCuePoints
(
adsManager
.
getAdCuePoints
());
hasAdPlaybackState
=
true
;
updateAdPlaybackState
();
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"onAdsManagerLoaded"
,
e
);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public
void
onAdEvent
(
AdEvent
adEvent
)
{
AdEventType
adEventType
=
adEvent
.
getType
();
if
(
DEBUG
&&
adEventType
!=
AdEventType
.
AD_PROGRESS
)
{
Log
.
d
(
TAG
,
"onAdEvent: "
+
adEventType
);
}
if
(
adsManager
==
null
)
{
// Drop events after release.
return
;
}
try
{
handleAdEvent
(
adEvent
);
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"onAdEvent"
,
e
);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public
void
onAdError
(
AdErrorEvent
adErrorEvent
)
{
AdError
error
=
adErrorEvent
.
getError
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"onAdError"
,
error
);
}
if
(
adsManager
==
null
)
{
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext
=
null
;
adPlaybackState
=
AdPlaybackState
.
NONE
;
hasAdPlaybackState
=
true
;
updateAdPlaybackState
();
}
else
if
(
isAdGroupLoadError
(
error
))
{
try
{
handleAdGroupLoadError
(
error
);
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"onAdError"
,
e
);
}
}
if
(
pendingAdLoadError
==
null
)
{
pendingAdLoadError
=
AdLoadException
.
createForAllAds
(
error
);
}
maybeNotifyPendingAdLoadError
();
}
// ContentProgressProvider implementation.
@Override
public
VideoProgressUpdate
getContentProgress
()
{
VideoProgressUpdate
videoProgressUpdate
=
getContentVideoProgressUpdate
();
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Content progress: "
+
videoProgressUpdate
);
}
if
(
waitingForPreloadElapsedRealtimeMs
!=
C
.
TIME_UNSET
)
{
// IMA is polling the player position but we are buffering for an ad to preload, so playback
// may be stuck. Detect this case and signal an error if applicable.
long
stuckElapsedRealtimeMs
=
SystemClock
.
elapsedRealtime
()
-
waitingForPreloadElapsedRealtimeMs
;
if
(
stuckElapsedRealtimeMs
>=
THRESHOLD_AD_PRELOAD_MS
)
{
waitingForPreloadElapsedRealtimeMs
=
C
.
TIME_UNSET
;
handleAdGroupLoadError
(
new
IOException
(
"Ad preloading timed out"
));
maybeNotifyPendingAdLoadError
();
}
}
return
videoProgressUpdate
;
}
// VideoAdPlayer implementation.
@Override
public
VideoProgressUpdate
getAdProgress
()
{
throw
new
IllegalStateException
(
"Unexpected call to getAdProgress when using preloading"
);
}
@Override
public
int
getVolume
()
{
return
getPlayerVolumePercent
();
}
@Override
public
void
loadAd
(
AdMediaInfo
adMediaInfo
,
AdPodInfo
adPodInfo
)
{
try
{
if
(
adsManager
==
null
)
{
// Drop events after release.
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"loadAd after release "
+
getAdMediaInfoString
(
adMediaInfo
)
+
", ad pod "
+
adPodInfo
);
}
return
;
}
int
adGroupIndex
=
getAdGroupIndexForAdPod
(
adPodInfo
);
int
adIndexInAdGroup
=
adPodInfo
.
getAdPosition
()
-
1
;
AdInfo
adInfo
=
new
AdInfo
(
adGroupIndex
,
adIndexInAdGroup
);
adInfoByAdMediaInfo
.
put
(
adMediaInfo
,
adInfo
);
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"loadAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adPlaybackState
.
isAdInErrorState
(
adGroupIndex
,
adIndexInAdGroup
))
{
// We have already marked this ad as having failed to load, so ignore the request. IMA
// will timeout after its media load timeout.
return
;
}
// The ad count may increase on successive loads of ads in the same ad pod, for example, due
// to separate requests for ad tags with multiple ads within the ad pod completing after an
// earlier ad has loaded. See also https://github.com/google/ExoPlayer/issues/7477.
AdPlaybackState
.
AdGroup
adGroup
=
adPlaybackState
.
adGroups
[
adInfo
.
adGroupIndex
];
adPlaybackState
=
adPlaybackState
.
withAdCount
(
adInfo
.
adGroupIndex
,
Math
.
max
(
adPodInfo
.
getTotalAds
(),
adGroup
.
states
.
length
));
adGroup
=
adPlaybackState
.
adGroups
[
adInfo
.
adGroupIndex
];
for
(
int
i
=
0
;
i
<
adIndexInAdGroup
;
i
++)
{
// Any preceding ads that haven't loaded are not going to load.
if
(
adGroup
.
states
[
i
]
==
AdPlaybackState
.
AD_STATE_UNAVAILABLE
)
{
adPlaybackState
=
adPlaybackState
.
withAdLoadError
(
adGroupIndex
,
/* adIndexInAdGroup= */
i
);
}
}
Uri
adUri
=
Uri
.
parse
(
adMediaInfo
.
getUrl
());
adPlaybackState
=
adPlaybackState
.
withAdUri
(
adInfo
.
adGroupIndex
,
adInfo
.
adIndexInAdGroup
,
adUri
);
updateAdPlaybackState
();
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"loadAd"
,
e
);
}
}
@Override
public
void
addCallback
(
VideoAdPlayerCallback
videoAdPlayerCallback
)
{
adCallbacks
.
add
(
videoAdPlayerCallback
);
}
@Override
public
void
removeCallback
(
VideoAdPlayerCallback
videoAdPlayerCallback
)
{
adCallbacks
.
remove
(
videoAdPlayerCallback
);
}
@Override
public
void
playAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"playAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adsManager
==
null
)
{
// Drop events after release.
return
;
}
if
(
imaAdState
==
IMA_AD_STATE_PLAYING
)
{
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log
.
w
(
TAG
,
"Unexpected playAd without stopAd"
);
}
try
{
if
(
imaAdState
==
IMA_AD_STATE_NONE
)
{
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs
=
C
.
TIME_UNSET
;
fakeContentProgressOffsetMs
=
C
.
TIME_UNSET
;
imaAdState
=
IMA_AD_STATE_PLAYING
;
imaAdMediaInfo
=
adMediaInfo
;
imaAdInfo
=
Assertions
.
checkNotNull
(
adInfoByAdMediaInfo
.
get
(
adMediaInfo
));
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onPlay
(
adMediaInfo
);
}
if
(
pendingAdPrepareErrorAdInfo
!=
null
&&
pendingAdPrepareErrorAdInfo
.
equals
(
imaAdInfo
))
{
pendingAdPrepareErrorAdInfo
=
null
;
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onError
(
adMediaInfo
);
}
}
updateAdProgress
();
}
else
{
imaAdState
=
IMA_AD_STATE_PLAYING
;
Assertions
.
checkState
(
adMediaInfo
.
equals
(
imaAdMediaInfo
));
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onResume
(
adMediaInfo
);
}
}
if
(!
Assertions
.
checkNotNull
(
player
).
getPlayWhenReady
())
{
Assertions
.
checkNotNull
(
adsManager
).
pause
();
}
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"playAd"
,
e
);
}
}
@Override
public
void
stopAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"stopAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adsManager
==
null
)
{
// Drop event after release.
return
;
}
if
(
imaAdState
==
IMA_AD_STATE_NONE
)
{
// This method is called if loadAd has been called but the preloaded ad won't play due to a
// seek to a different position, so drop the event and discard the ad. See also [Internal:
// b/159111848].
@Nullable
AdInfo
adInfo
=
adInfoByAdMediaInfo
.
get
(
adMediaInfo
);
if
(
adInfo
!=
null
)
{
adPlaybackState
=
adPlaybackState
.
withSkippedAd
(
adInfo
.
adGroupIndex
,
adInfo
.
adIndexInAdGroup
);
updateAdPlaybackState
();
}
return
;
}
try
{
Assertions
.
checkNotNull
(
player
);
stopAdInternal
();
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"stopAd"
,
e
);
}
}
@Override
public
void
pauseAd
(
AdMediaInfo
adMediaInfo
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"pauseAd "
+
getAdMediaInfoString
(
adMediaInfo
));
}
if
(
adsManager
==
null
)
{
// Drop event after release.
return
;
}
if
(
imaAdState
==
IMA_AD_STATE_NONE
)
{
// This method is called if loadAd has been called but the loaded ad won't play due to a
// seek to a different position, so drop the event. See also [Internal: b/159111848].
return
;
}
try
{
Assertions
.
checkState
(
adMediaInfo
.
equals
(
imaAdMediaInfo
));
imaAdState
=
IMA_AD_STATE_PAUSED
;
for
(
int
i
=
0
;
i
<
adCallbacks
.
size
();
i
++)
{
adCallbacks
.
get
(
i
).
onPause
(
adMediaInfo
);
}
}
catch
(
RuntimeException
e
)
{
maybeNotifyInternalError
(
"pauseAd"
,
e
);
}
}
@Override
public
void
release
()
{
// Do nothing.
}
}
// TODO: Consider moving this into AdPlaybackState.
private
static
final
class
AdInfo
{
public
final
int
adGroupIndex
;
...
...
extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
View file @
1a5b304b
...
...
@@ -17,10 +17,12 @@ package com.google.android.exoplayer2.ext.ima;
import
static
com
.
google
.
common
.
truth
.
Truth
.
assertThat
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
import
static
org
.
mockito
.
ArgumentMatchers
.
anyDouble
;
import
static
org
.
mockito
.
Mockito
.
atLeastOnce
;
import
static
org
.
mockito
.
Mockito
.
doAnswer
;
import
static
org
.
mockito
.
Mockito
.
doNothing
;
import
static
org
.
mockito
.
Mockito
.
inOrder
;
import
static
org
.
mockito
.
Mockito
.
never
;
import
static
org
.
mockito
.
Mockito
.
verify
;
import
static
org
.
mockito
.
Mockito
.
when
;
...
...
@@ -42,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import
com.google.ads.interactivemedia.v3.api.AdsRequest
;
import
com.google.ads.interactivemedia.v3.api.ImaSdkSettings
;
import
com.google.ads.interactivemedia.v3.api.player.AdMediaInfo
;
import
com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider
;
import
com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.ExoPlaybackException
;
import
com.google.android.exoplayer2.Player
;
...
...
@@ -56,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import
com.google.android.exoplayer2.testutil.FakeTimeline
;
import
com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition
;
import
com.google.android.exoplayer2.upstream.DataSpec
;
import
com.google.common.collect.ImmutableList
;
import
com.google.common.collect.ImmutableMap
;
import
java.io.IOException
;
import
java.time.Duration
;
...
...
@@ -91,8 +96,7 @@ public final class ImaAdsLoaderTest {
private
static
final
Uri
TEST_URI
=
Uri
.
EMPTY
;
private
static
final
AdMediaInfo
TEST_AD_MEDIA_INFO
=
new
AdMediaInfo
(
TEST_URI
.
toString
());
private
static
final
long
TEST_AD_DURATION_US
=
5
*
C
.
MICROS_PER_SECOND
;
private
static
final
long
[][]
ADS_DURATIONS_US
=
new
long
[][]
{{
TEST_AD_DURATION_US
}};
private
static
final
Float
[]
PREROLL_CUE_POINTS_SECONDS
=
new
Float
[]
{
0
f
};
private
static
final
ImmutableList
<
Float
>
PREROLL_CUE_POINTS_SECONDS
=
ImmutableList
.
of
(
0
f
);
@Rule
public
final
MockitoRule
mockito
=
MockitoJUnit
.
rule
();
...
...
@@ -111,6 +115,9 @@ public final class ImaAdsLoaderTest {
private
ViewGroup
adViewGroup
;
private
View
adOverlayView
;
private
AdsLoader
.
AdViewProvider
adViewProvider
;
private
AdEvent
.
AdEventListener
adEventListener
;
private
ContentProgressProvider
contentProgressProvider
;
private
VideoAdPlayer
videoAdPlayer
;
private
TestAdsLoaderListener
adsLoaderListener
;
private
FakePlayer
fakeExoPlayer
;
private
ImaAdsLoader
imaAdsLoader
;
...
...
@@ -144,14 +151,14 @@ public final class ImaAdsLoaderTest {
@Test
public
void
builder_overridesPlayerType
()
{
when
(
mockImaSdkSettings
.
getPlayerType
()).
thenReturn
(
"test player type"
);
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
verify
(
mockImaSdkSettings
).
setPlayerType
(
"google/exo.ext.ima"
);
}
@Test
public
void
start_setsAdUiViewGroup
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
verify
(
mockAdDisplayContainer
,
atLeastOnce
()).
setAdContainer
(
adViewGroup
);
...
...
@@ -161,7 +168,7 @@ public final class ImaAdsLoaderTest {
@Test
public
void
start_withPlaceholderContent_initializedAdsLoader
()
{
Timeline
placeholderTimeline
=
new
DummyTimeline
(
/* tag= */
null
);
setupPlayback
(
placeholderTimeline
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
placeholderTimeline
,
PREROLL_CUE_POINTS_SECONDS
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
// We'll only create the rendering settings when initializing the ads loader.
...
...
@@ -170,26 +177,27 @@ public final class ImaAdsLoaderTest {
@Test
public
void
start_updatesAdPlaybackState
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
new
AdPlaybackState
(
/* adGroupTimesUs...= */
0
)
.
withAdDurationsUs
(
ADS_DURATIONS_US
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
startAfterRelease
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
imaAdsLoader
.
release
();
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
}
@Test
public
void
startAndCallbacksAfterRelease
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
// Request ads in order to get a reference to the ad event listener.
imaAdsLoader
.
requestAds
(
adViewGroup
);
imaAdsLoader
.
release
();
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
fakeExoPlayer
.
setPlayingContentPosition
(
/* position= */
0
);
...
...
@@ -200,47 +208,47 @@ public final class ImaAdsLoaderTest {
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded.
imaAdsLoader
.
requestAds
(
adViewGroup
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
LOADED
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
loadAd
(
TEST_AD_MEDIA_INFO
,
mockAdPodInfo
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_PAUSE_REQUESTED
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
playAd
(
TEST_AD_MEDIA_INFO
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
STARTED
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
pauseAd
(
TEST_AD_MEDIA_INFO
);
imaAdsLoad
er
.
stopAd
(
TEST_AD_MEDIA_INFO
);
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
LOADED
,
mockPrerollSingleAd
));
videoAdPlay
er
.
loadAd
(
TEST_AD_MEDIA_INFO
,
mockAdPodInfo
);
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_PAUSE_REQUESTED
,
mockPrerollSingleAd
));
videoAdPlay
er
.
playAd
(
TEST_AD_MEDIA_INFO
);
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
STARTED
,
mockPrerollSingleAd
));
videoAdPlay
er
.
pauseAd
(
TEST_AD_MEDIA_INFO
);
videoAdPlay
er
.
stopAd
(
TEST_AD_MEDIA_INFO
);
imaAdsLoader
.
onPlayerError
(
ExoPlaybackException
.
createForSource
(
new
IOException
()));
imaAdsLoader
.
onPositionDiscontinuity
(
Player
.
DISCONTINUITY_REASON_SEEK
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_RESUME_REQUESTED
,
/* ad= */
null
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_RESUME_REQUESTED
,
/* ad= */
null
));
imaAdsLoader
.
handlePrepareError
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
,
new
IOException
());
}
@Test
public
void
playback_withPrerollAd_marksAdAsPlayed
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
// Load the preroll ad.
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
LOADED
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
loadAd
(
TEST_AD_MEDIA_INFO
,
mockAdPodInfo
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_PAUSE_REQUESTED
,
mockPrerollSingleAd
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
LOADED
,
mockPrerollSingleAd
));
videoAdPlay
er
.
loadAd
(
TEST_AD_MEDIA_INFO
,
mockAdPodInfo
);
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_PAUSE_REQUESTED
,
mockPrerollSingleAd
));
// Play the preroll ad.
imaAdsLoad
er
.
playAd
(
TEST_AD_MEDIA_INFO
);
videoAdPlay
er
.
playAd
(
TEST_AD_MEDIA_INFO
);
fakeExoPlayer
.
setPlayingAdPosition
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
,
/* position= */
0
,
/* contentPosition= */
0
);
fakeExoPlayer
.
setState
(
Player
.
STATE_READY
,
true
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
STARTED
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
FIRST_QUARTILE
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
MIDPOINT
,
mockPrerollSingleAd
));
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
THIRD_QUARTILE
,
mockPrerollSingleAd
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
STARTED
,
mockPrerollSingleAd
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
FIRST_QUARTILE
,
mockPrerollSingleAd
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
MIDPOINT
,
mockPrerollSingleAd
));
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
THIRD_QUARTILE
,
mockPrerollSingleAd
));
// Play the content.
fakeExoPlayer
.
setPlayingContentPosition
(
0
);
imaAdsLoad
er
.
stopAd
(
TEST_AD_MEDIA_INFO
);
imaAdsLoad
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_RESUME_REQUESTED
,
/* ad= */
null
));
videoAdPlay
er
.
stopAd
(
TEST_AD_MEDIA_INFO
);
adEventListen
er
.
onAdEvent
(
getAdEvent
(
AdEventType
.
CONTENT_RESUME_REQUESTED
,
/* ad= */
null
));
// Verify that the preroll ad has been marked as played.
assertThat
(
adsLoaderListener
.
adPlaybackState
)
...
...
@@ -249,24 +257,24 @@ public final class ImaAdsLoaderTest {
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withAdCount
(
/* adGroupIndex= */
0
,
/* adCount= */
1
)
.
withAdUri
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
,
/* uri= */
TEST_URI
)
.
withAdDurationsUs
(
ADS_DURATIONS_US
)
.
withAdDurationsUs
(
new
long
[][]
{{
TEST_AD_DURATION_US
}}
)
.
withPlayedAd
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
)
.
withAdResumePositionUs
(
/* adResumePositionUs= */
0
));
}
@Test
public
void
playback_withPostrollFetchError_marksAdAsInErrorState
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
new
Float
[]
{-
1
f
}
);
setupPlayback
(
CONTENT_TIMELINE
,
ImmutableList
.
of
(-
1
f
)
);
// Simulate loading an empty postroll ad.
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
imaAdsLoad
er
.
onAdEvent
(
mockPostrollFetchErrorAdEvent
);
adEventListen
er
.
onAdEvent
(
mockPostrollFetchErrorAdEvent
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
new
AdPlaybackState
(
/* adGroupTimesUs...= */
C
.
TIME_END_OF_SOURCE
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withAdDurationsUs
(
ADS_DURATIONS_US
)
.
withAdDurationsUs
(
new
long
[][]
{{
TEST_AD_DURATION_US
}}
)
.
withAdCount
(
/* adGroupIndex= */
0
,
/* adCount= */
1
)
.
withAdLoadError
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
));
}
...
...
@@ -275,10 +283,9 @@ public final class ImaAdsLoaderTest {
public
void
playback_withAdNotPreloadingBeforeTimeout_hasNoError
()
{
// Simulate an ad at 2 seconds.
long
adGroupPositionInWindowUs
=
2
*
C
.
MICROS_PER_SECOND
;
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
new
Float
[]
{(
float
)
adGroupPositionInWindowUs
/
C
.
MICROS_PER_SECOND
});
long
adGroupTimeUs
=
adGroupPositionInWindowUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
((
float
)
adGroupTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
...
...
@@ -286,23 +293,21 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer
.
setState
(
Player
.
STATE_BUFFERING
,
/* playWhenReady= */
true
);
// Advance before the timeout and simulating polling content progress.
ShadowSystemClock
.
advanceBy
(
Duration
.
ofSeconds
(
1
));
imaAdsLoa
der
.
getContentProgress
();
contentProgressProvi
der
.
getContentProgress
();
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
new
AdPlaybackState
(
/* adGroupTimesUs...= */
adGroupPositionInWindowUs
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withAdDurationsUs
(
ADS_DURATIONS_US
));
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup
()
{
// Simulate an ad at 2 seconds.
long
adGroupPositionInWindowUs
=
2
*
C
.
MICROS_PER_SECOND
;
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
new
Float
[]
{(
float
)
adGroupPositionInWindowUs
/
C
.
MICROS_PER_SECOND
});
long
adGroupTimeUs
=
adGroupPositionInWindowUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
((
float
)
adGroupTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
...
...
@@ -310,20 +315,296 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer
.
setState
(
Player
.
STATE_BUFFERING
,
/* playWhenReady= */
true
);
// Advance past the timeout and simulate polling content progress.
ShadowSystemClock
.
advanceBy
(
Duration
.
ofSeconds
(
5
));
imaAdsLoa
der
.
getContentProgress
();
contentProgressProvi
der
.
getContentProgress
();
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
new
AdPlaybackState
(
/* adGroupTimesUs...= */
adGroupPositionInWindowU
s
)
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoint
s
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withAdDurationsUs
(
ADS_DURATIONS_US
)
.
withAdDurationsUs
(
new
long
[][]
{{
TEST_AD_DURATION_US
}}
)
.
withAdCount
(
/* adGroupIndex= */
0
,
/* adCount= */
1
)
.
withAdLoadError
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
));
}
@Test
public
void
resumePlaybackBeforeMidroll_playsPreroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
)
-
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
verify
(
mockAdsRenderingSettings
,
never
()).
setPlayAdsAfterTime
(
anyDouble
());
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
resumePlaybackAtMidroll_skipsPreroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
));
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
midrollPeriodTimeUs
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
));
}
@Test
public
void
resumePlaybackAfterMidroll_skipsPreroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
)
+
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
midrollPeriodTimeUs
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
));
}
@Test
public
void
resumePlaybackBeforeSecondMidroll_playsFirstMidroll
()
{
long
firstMidrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
firstMidrollPeriodTimeUs
=
firstMidrollWindowTimeUs
;
long
secondMidrollWindowTimeUs
=
4
*
C
.
MICROS_PER_SECOND
;
long
secondMidrollPeriodTimeUs
=
secondMidrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
(
float
)
firstMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
,
(
float
)
secondMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
secondMidrollWindowTimeUs
)
-
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
verify
(
mockAdsRenderingSettings
,
never
()).
setPlayAdsAfterTime
(
anyDouble
());
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
resumePlaybackAtSecondMidroll_skipsFirstMidroll
()
{
long
firstMidrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
firstMidrollPeriodTimeUs
=
firstMidrollWindowTimeUs
;
long
secondMidrollWindowTimeUs
=
4
*
C
.
MICROS_PER_SECOND
;
long
secondMidrollPeriodTimeUs
=
secondMidrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
(
float
)
firstMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
,
(
float
)
secondMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
secondMidrollWindowTimeUs
));
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
(
firstMidrollPeriodTimeUs
+
secondMidrollPeriodTimeUs
)
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
));
}
@Test
public
void
resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setPlayAdBeforeStartPosition
(
false
)
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
));
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
)
-
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
midrollPeriodTimeUs
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1d
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setPlayAdBeforeStartPosition
(
false
)
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
));
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
));
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
midrollPeriodTimeUs
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1d
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
));
}
@Test
public
void
resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMidroll
()
{
long
midrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
midrollPeriodTimeUs
=
midrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
0
f
,
(
float
)
midrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setPlayAdBeforeStartPosition
(
false
)
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
));
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
midrollWindowTimeUs
)
+
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
verify
(
mockAdsManager
).
destroy
();
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
1
));
}
@Test
public
void
resumePlaybackBeforeSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll
()
{
long
firstMidrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
firstMidrollPeriodTimeUs
=
firstMidrollWindowTimeUs
;
long
secondMidrollWindowTimeUs
=
4
*
C
.
MICROS_PER_SECOND
;
long
secondMidrollPeriodTimeUs
=
secondMidrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
(
float
)
firstMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
,
(
float
)
secondMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setPlayAdBeforeStartPosition
(
false
)
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
));
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
secondMidrollWindowTimeUs
)
-
1_000
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
(
firstMidrollPeriodTimeUs
+
secondMidrollPeriodTimeUs
)
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1d
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
));
}
@Test
public
void
resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll
()
{
long
firstMidrollWindowTimeUs
=
2
*
C
.
MICROS_PER_SECOND
;
long
firstMidrollPeriodTimeUs
=
firstMidrollWindowTimeUs
;
long
secondMidrollWindowTimeUs
=
4
*
C
.
MICROS_PER_SECOND
;
long
secondMidrollPeriodTimeUs
=
secondMidrollWindowTimeUs
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
(
float
)
firstMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
,
(
float
)
secondMidrollPeriodTimeUs
/
C
.
MICROS_PER_SECOND
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setPlayAdBeforeStartPosition
(
false
)
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
));
fakeExoPlayer
.
setPlayingContentPosition
(
C
.
usToMs
(
secondMidrollWindowTimeUs
));
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
ArgumentCaptor
<
Double
>
playAdsAfterTimeCaptor
=
ArgumentCaptor
.
forClass
(
Double
.
class
);
verify
(
mockAdsRenderingSettings
).
setPlayAdsAfterTime
(
playAdsAfterTimeCaptor
.
capture
());
double
expectedPlayAdsAfterTimeUs
=
(
firstMidrollPeriodTimeUs
+
secondMidrollPeriodTimeUs
)
/
2
d
;
assertThat
(
playAdsAfterTimeCaptor
.
getValue
())
.
isWithin
(
0.1d
)
.
of
(
expectedPlayAdsAfterTimeUs
/
C
.
MICROS_PER_SECOND
);
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withSkippedAdGroup
(
/* adGroupIndex= */
0
));
}
@Test
public
void
stop_unregistersAllVideoControlOverlays
()
{
setupPlayback
(
CONTENT_TIMELINE
,
ADS_DURATIONS_US
,
PREROLL_CUE_POINTS_SECONDS
);
setupPlayback
(
CONTENT_TIMELINE
,
PREROLL_CUE_POINTS_SECONDS
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
imaAdsLoader
.
requestAds
(
adViewGroup
);
imaAdsLoader
.
stop
();
...
...
@@ -333,15 +614,71 @@ public final class ImaAdsLoaderTest {
inOrder
.
verify
(
mockAdDisplayContainer
).
unregisterAllVideoControlsOverlays
();
}
private
void
setupPlayback
(
Timeline
contentTimeline
,
long
[][]
adDurationsUs
,
Float
[]
cuePoints
)
{
fakeExoPlayer
=
new
FakePlayer
();
adsLoaderListener
=
new
TestAdsLoaderListener
(
fakeExoPlayer
,
contentTimeline
,
adDurationsUs
);
when
(
mockAdsManager
.
getAdCuePoints
()).
thenReturn
(
Arrays
.
asList
(
cuePoints
));
imaAdsLoader
=
@Test
public
void
loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd
()
{
float
midrollTimeSecs
=
1_765
f
;
ImmutableList
<
Float
>
cuePoints
=
ImmutableList
.
of
(
midrollTimeSecs
);
setupPlayback
(
CONTENT_TIMELINE
,
cuePoints
);
imaAdsLoader
.
start
(
adsLoaderListener
,
adViewProvider
);
videoAdPlayer
.
loadAd
(
TEST_AD_MEDIA_INFO
,
new
AdPodInfo
()
{
@Override
public
int
getTotalAds
()
{
return
1
;
}
@Override
public
int
getAdPosition
()
{
return
1
;
}
@Override
public
boolean
isBumper
()
{
return
false
;
}
@Override
public
double
getMaxDuration
()
{
return
0
;
}
@Override
public
int
getPodIndex
()
{
return
0
;
}
@Override
public
double
getTimeOffset
()
{
return
midrollTimeSecs
;
}
});
assertThat
(
adsLoaderListener
.
adPlaybackState
)
.
isEqualTo
(
AdPlaybackStateFactory
.
fromCuePoints
(
cuePoints
)
.
withContentDurationUs
(
CONTENT_PERIOD_DURATION_US
)
.
withAdCount
(
/* adGroupIndex= */
0
,
/* adCount= */
1
)
.
withAdUri
(
/* adGroupIndex= */
0
,
/* adIndexInAdGroup= */
0
,
TEST_URI
)
.
withAdDurationsUs
(
new
long
[][]
{{
TEST_AD_DURATION_US
}}));
}
private
void
setupPlayback
(
Timeline
contentTimeline
,
List
<
Float
>
cuePoints
)
{
setupPlayback
(
contentTimeline
,
cuePoints
,
new
ImaAdsLoader
.
Builder
(
ApplicationProvider
.
getApplicationContext
())
.
setImaFactory
(
mockImaFactory
)
.
setImaSdkSettings
(
mockImaSdkSettings
)
.
buildForAdTag
(
TEST_URI
);
.
buildForAdTag
(
TEST_URI
));
}
private
void
setupPlayback
(
Timeline
contentTimeline
,
List
<
Float
>
cuePoints
,
ImaAdsLoader
imaAdsLoader
)
{
fakeExoPlayer
=
new
FakePlayer
();
adsLoaderListener
=
new
TestAdsLoaderListener
(
fakeExoPlayer
,
contentTimeline
);
when
(
mockAdsManager
.
getAdCuePoints
()).
thenReturn
(
cuePoints
);
this
.
imaAdsLoader
=
imaAdsLoader
;
imaAdsLoader
.
setPlayer
(
fakeExoPlayer
);
}
...
...
@@ -349,9 +686,11 @@ public final class ImaAdsLoaderTest {
ArgumentCaptor
<
Object
>
userRequestContextCaptor
=
ArgumentCaptor
.
forClass
(
Object
.
class
);
doNothing
().
when
(
mockAdsRequest
).
setUserRequestContext
(
userRequestContextCaptor
.
capture
());
when
(
mockAdsRequest
.
getUserRequestContext
())
.
thenAnswer
(
(
Answer
<
Object
>)
invocation
->
userRequestContextCaptor
.
getValue
());
.
thenAnswer
(
invocation
->
userRequestContextCaptor
.
getValue
());
List
<
com
.
google
.
ads
.
interactivemedia
.
v3
.
api
.
AdsLoader
.
AdsLoadedListener
>
adsLoadedListeners
=
new
ArrayList
<>();
// Deliberately don't handle removeAdsLoadedListener to allow testing behavior if the IMA SDK
// invokes callbacks after release.
doAnswer
(
invocation
->
{
adsLoadedListeners
.
add
(
invocation
.
getArgument
(
0
));
...
...
@@ -359,13 +698,6 @@ public final class ImaAdsLoaderTest {
})
.
when
(
mockAdsLoader
)
.
addAdsLoadedListener
(
any
());
doAnswer
(
invocation
->
{
adsLoadedListeners
.
remove
(
invocation
.
getArgument
(
0
));
return
null
;
})
.
when
(
mockAdsLoader
)
.
removeAdsLoadedListener
(
any
());
when
(
mockAdsManagerLoadedEvent
.
getAdsManager
()).
thenReturn
(
mockAdsManager
);
when
(
mockAdsManagerLoadedEvent
.
getUserRequestContext
())
.
thenAnswer
(
invocation
->
mockAdsRequest
.
getUserRequestContext
());
...
...
@@ -381,6 +713,30 @@ public final class ImaAdsLoaderTest {
.
when
(
mockAdsLoader
)
.
requestAds
(
mockAdsRequest
);
doAnswer
(
invocation
->
{
adEventListener
=
invocation
.
getArgument
(
0
);
return
null
;
})
.
when
(
mockAdsManager
)
.
addAdEventListener
(
any
());
doAnswer
(
invocation
->
{
contentProgressProvider
=
invocation
.
getArgument
(
0
);
return
null
;
})
.
when
(
mockAdsRequest
)
.
setContentProgressProvider
(
any
());
doAnswer
(
invocation
->
{
videoAdPlayer
=
invocation
.
getArgument
(
0
);
return
null
;
})
.
when
(
mockAdDisplayContainer
)
.
setPlayer
(
any
());
when
(
mockImaFactory
.
createAdDisplayContainer
()).
thenReturn
(
mockAdDisplayContainer
);
when
(
mockImaFactory
.
createAdsRenderingSettings
()).
thenReturn
(
mockAdsRenderingSettings
);
when
(
mockImaFactory
.
createAdsRequest
()).
thenReturn
(
mockAdsRequest
);
...
...
@@ -422,19 +778,21 @@ public final class ImaAdsLoaderTest {
private
final
FakePlayer
fakeExoPlayer
;
private
final
Timeline
contentTimeline
;
private
final
long
[][]
adDurationsUs
;
public
AdPlaybackState
adPlaybackState
;
public
TestAdsLoaderListener
(
FakePlayer
fakeExoPlayer
,
Timeline
contentTimeline
,
long
[][]
adDurationsUs
)
{
public
TestAdsLoaderListener
(
FakePlayer
fakeExoPlayer
,
Timeline
contentTimeline
)
{
this
.
fakeExoPlayer
=
fakeExoPlayer
;
this
.
contentTimeline
=
contentTimeline
;
this
.
adDurationsUs
=
adDurationsUs
;
}
@Override
public
void
onAdPlaybackState
(
AdPlaybackState
adPlaybackState
)
{
long
[][]
adDurationsUs
=
new
long
[
adPlaybackState
.
adGroupCount
][];
for
(
int
adGroupIndex
=
0
;
adGroupIndex
<
adPlaybackState
.
adGroupCount
;
adGroupIndex
++)
{
adDurationsUs
[
adGroupIndex
]
=
new
long
[
adPlaybackState
.
adGroups
[
adGroupIndex
].
uris
.
length
];
Arrays
.
fill
(
adDurationsUs
[
adGroupIndex
],
TEST_AD_DURATION_US
);
}
adPlaybackState
=
adPlaybackState
.
withAdDurationsUs
(
adDurationsUs
);
this
.
adPlaybackState
=
adPlaybackState
;
fakeExoPlayer
.
updateTimeline
(
...
...
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
View file @
1a5b304b
...
...
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public
static
final
String
VERSION
=
"2.11.
5
"
;
public
static
final
String
VERSION
=
"2.11.
6
"
;
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public
static
final
String
VERSION_SLASHY
=
"ExoPlayerLib/2.11.
5
"
;
public
static
final
String
VERSION_SLASHY
=
"ExoPlayerLib/2.11.
6
"
;
/**
* The version of the library expressed as an integer, for example 1002003.
...
...
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public
static
final
int
VERSION_INT
=
201100
5
;
public
static
final
int
VERSION_INT
=
201100
6
;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
...
...
library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
View file @
1a5b304b
...
...
@@ -124,13 +124,9 @@ public final class AdPlaybackState {
return
result
;
}
/**
* Returns a new instance with the ad count set to {@code count}. This method may only be called
* if this instance's ad count has not yet been specified.
*/
/** Returns a new instance with the ad count set to {@code count}. */
@CheckResult
public
AdGroup
withAdCount
(
int
count
)
{
Assertions
.
checkArgument
(
this
.
count
==
C
.
LENGTH_UNSET
&&
states
.
length
<=
count
);
@AdState
int
[]
states
=
copyStatesWithSpaceForAdCount
(
this
.
states
,
count
);
long
[]
durationsUs
=
copyDurationsUsWithSpaceForAdCount
(
this
.
durationsUs
,
count
);
@NullableType
Uri
[]
uris
=
Arrays
.
copyOf
(
this
.
uris
,
count
);
...
...
@@ -139,17 +135,11 @@ public final class AdPlaybackState {
/**
* Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
* marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
* #AD_STATE_UNAVAILABLE}, which is the default state.
*
* <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
* ad count specified later. Otherwise, {@code index} must be less than the current ad count.
* marked as {@link #AD_STATE_AVAILABLE}.
*/
@CheckResult
public
AdGroup
withAdUri
(
Uri
uri
,
int
index
)
{
Assertions
.
checkArgument
(
count
==
C
.
LENGTH_UNSET
||
index
<
count
);
@AdState
int
[]
states
=
copyStatesWithSpaceForAdCount
(
this
.
states
,
index
+
1
);
Assertions
.
checkArgument
(
states
[
index
]
==
AD_STATE_UNAVAILABLE
);
long
[]
durationsUs
=
this
.
durationsUs
.
length
==
states
.
length
?
this
.
durationsUs
...
...
@@ -280,7 +270,9 @@ public final class AdPlaybackState {
public
final
AdGroup
[]
adGroups
;
/** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
public
final
long
adResumePositionUs
;
/** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
/**
* The duration of the content period in microseconds, if known. {@link C#TIME_UNSET} otherwise.
*/
public
final
long
contentDurationUs
;
/**
...
...
@@ -489,6 +481,54 @@ public final class AdPlaybackState {
return
result
;
}
@Override
public
String
toString
()
{
StringBuilder
sb
=
new
StringBuilder
();
sb
.
append
(
"AdPlaybackState(adResumePositionUs="
);
sb
.
append
(
adResumePositionUs
);
sb
.
append
(
", adGroups=["
);
for
(
int
i
=
0
;
i
<
adGroups
.
length
;
i
++)
{
sb
.
append
(
"adGroup(timeUs="
);
sb
.
append
(
adGroupTimesUs
[
i
]);
sb
.
append
(
", ads=["
);
for
(
int
j
=
0
;
j
<
adGroups
[
i
].
states
.
length
;
j
++)
{
sb
.
append
(
"ad(state="
);
switch
(
adGroups
[
i
].
states
[
j
])
{
case
AD_STATE_UNAVAILABLE:
sb
.
append
(
'_'
);
break
;
case
AD_STATE_ERROR:
sb
.
append
(
'!'
);
break
;
case
AD_STATE_AVAILABLE:
sb
.
append
(
'R'
);
break
;
case
AD_STATE_PLAYED:
sb
.
append
(
'P'
);
break
;
case
AD_STATE_SKIPPED:
sb
.
append
(
'S'
);
break
;
default
:
sb
.
append
(
'?'
);
break
;
}
sb
.
append
(
", durationUs="
);
sb
.
append
(
adGroups
[
i
].
durationsUs
[
j
]);
sb
.
append
(
')'
);
if
(
j
<
adGroups
[
i
].
states
.
length
-
1
)
{
sb
.
append
(
", "
);
}
}
sb
.
append
(
"])"
);
if
(
i
<
adGroups
.
length
-
1
)
{
sb
.
append
(
", "
);
}
}
sb
.
append
(
"])"
);
return
sb
.
toString
();
}
private
boolean
isPositionBeforeAdGroup
(
long
positionUs
,
long
periodDurationUs
,
int
adGroupIndex
)
{
if
(
positionUs
==
C
.
TIME_END_OF_SOURCE
)
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
View file @
1a5b304b
...
...
@@ -44,23 +44,16 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline {
@Override
public
Period
getPeriod
(
int
periodIndex
,
Period
period
,
boolean
setIds
)
{
timeline
.
getPeriod
(
periodIndex
,
period
,
setIds
);
long
durationUs
=
period
.
durationUs
==
C
.
TIME_UNSET
?
adPlaybackState
.
contentDurationUs
:
period
.
durationUs
;
period
.
set
(
period
.
id
,
period
.
uid
,
period
.
windowIndex
,
period
.
durationUs
,
durationUs
,
period
.
getPositionInWindowUs
(),
adPlaybackState
);
return
period
;
}
@Override
public
Window
getWindow
(
int
windowIndex
,
Window
window
,
long
defaultPositionProjectionUs
)
{
window
=
super
.
getWindow
(
windowIndex
,
window
,
defaultPositionProjectionUs
);
if
(
window
.
durationUs
==
C
.
TIME_UNSET
)
{
window
.
durationUs
=
adPlaybackState
.
contentDurationUs
;
}
return
window
;
}
}
library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
View file @
1a5b304b
...
...
@@ -155,12 +155,22 @@ public final class MimeTypes {
if
(
mimeType
==
null
)
{
return
false
;
}
// TODO: Consider adding additional audio MIME types here.
// TODO: Add additional audio MIME types. Also consider evaluating based on Format rather than
// just MIME type, since in some cases the property is true for a subset of the profiles
// belonging to a single MIME type. If we do this, we should move the method to a different
// class. See [Internal ref: http://go/exo-audio-format-random-access].
switch
(
mimeType
)
{
case
AUDIO_AAC:
case
AUDIO_MPEG:
case
AUDIO_MPEG_L1:
case
AUDIO_MPEG_L2:
case
AUDIO_RAW:
case
AUDIO_ALAW:
case
AUDIO_MLAW:
case
AUDIO_OPUS:
case
AUDIO_FLAC:
case
AUDIO_AC3:
case
AUDIO_E_AC3:
case
AUDIO_E_AC3_JOC:
return
true
;
default
:
return
false
;
...
...
library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
View file @
1a5b304b
...
...
@@ -48,6 +48,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import
com.google.android.exoplayer2.PlaybackPreparer
;
import
com.google.android.exoplayer2.Player
;
import
com.google.android.exoplayer2.Player.DiscontinuityReason
;
import
com.google.android.exoplayer2.Timeline
;
import
com.google.android.exoplayer2.Timeline.Period
;
import
com.google.android.exoplayer2.metadata.Metadata
;
import
com.google.android.exoplayer2.metadata.flac.PictureFrame
;
import
com.google.android.exoplayer2.metadata.id3.ApicFrame
;
...
...
@@ -1506,6 +1508,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
SingleTapListener
,
PlayerControlView
.
VisibilityListener
{
private
final
Period
period
;
private
@Nullable
Object
lastPeriodUidWithTracks
;
public
ComponentListener
()
{
period
=
new
Period
();
}
// TextOutput implementation
@Override
...
...
@@ -1554,6 +1563,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Override
public
void
onTracksChanged
(
TrackGroupArray
tracks
,
TrackSelectionArray
selections
)
{
// Suppress the update if transitioning to an unprepared period within the same window. This
// is necessary to avoid closing the shutter when such a transition occurs. See:
// https://github.com/google/ExoPlayer/issues/5507.
Player
player
=
Assertions
.
checkNotNull
(
PlayerView
.
this
.
player
);
Timeline
timeline
=
player
.
getCurrentTimeline
();
if
(
timeline
.
isEmpty
())
{
lastPeriodUidWithTracks
=
null
;
}
else
if
(!
player
.
getCurrentTrackGroups
().
isEmpty
())
{
lastPeriodUidWithTracks
=
timeline
.
getPeriod
(
player
.
getCurrentPeriodIndex
(),
period
,
/* setIds= */
true
).
uid
;
}
else
if
(
lastPeriodUidWithTracks
!=
null
)
{
int
lastPeriodIndexWithTracks
=
timeline
.
getIndexOfPeriod
(
lastPeriodUidWithTracks
);
if
(
lastPeriodIndexWithTracks
!=
C
.
INDEX_UNSET
)
{
int
lastWindowIndexWithTracks
=
timeline
.
getPeriod
(
lastPeriodIndexWithTracks
,
period
).
windowIndex
;
if
(
player
.
getCurrentWindowIndex
()
==
lastWindowIndexWithTracks
)
{
// We're in the same window. Suppress the update.
return
;
}
}
lastPeriodUidWithTracks
=
null
;
}
updateForCurrentTrackSelections
(
/* isNewPlayer= */
false
);
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment