mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add HlsInterstitialsAdsLoader
Reads HLS interstitials information from the playlist and populates the `AdPlaybackState` accordingly to play the ads. An app can register a `Listener` to be informed about ad related events. Only VOD streams are supported and X-ASSET-LIST attibutes are ignored with this change. PiperOrigin-RevId: 705604201
This commit is contained in:
parent
e0496ff88d
commit
83b5eb0040
@ -844,6 +844,13 @@ public final class AdPlaybackState {
|
|||||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns an instance with the specified value for {@link #adsId}. */
|
||||||
|
@CheckResult
|
||||||
|
public AdPlaybackState withAdsId(Object adsId) {
|
||||||
|
return new AdPlaybackState(
|
||||||
|
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
|
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
|
||||||
*
|
*
|
||||||
|
@ -168,11 +168,11 @@ public interface AdsLoader {
|
|||||||
* AdViewProvider, boolean) AdsMediaSource} to indicate the content source needs to be prepared
|
* AdViewProvider, boolean) AdsMediaSource} to indicate the content source needs to be prepared
|
||||||
* upfront.
|
* upfront.
|
||||||
*
|
*
|
||||||
* @param mediaItem The {@link MediaItem} of the source that produced the timeline.
|
* @param adsMediaSource The ads media source for which the content timeline changed.
|
||||||
* @param timeline The timeline of the content source.
|
* @param timeline The timeline of the content source.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
default void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
|
default void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,8 +201,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
* @param adViewProvider Provider of views for the ad UI.
|
* @param adViewProvider Provider of views for the ad UI.
|
||||||
* @param useLazyContentSourcePreparation True if the content source should be prepared lazily and
|
* @param useLazyContentSourcePreparation True if the content source should be prepared lazily and
|
||||||
* wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
|
* wait for an {@link AdPlaybackState} to be set before preparing. False if the timeline is
|
||||||
* required {@linkplain AdsLoader#handleContentTimelineChanged(MediaItem, Timeline) to read ad
|
* required {@linkplain AdsLoader#handleContentTimelineChanged(AdsMediaSource, Timeline) to
|
||||||
* data from it} to populate the {@link AdPlaybackState} (for instance from HLS
|
* read ad data from it} to populate the {@link AdPlaybackState} (for instance from HLS
|
||||||
* interstitials).
|
* interstitials).
|
||||||
*/
|
*/
|
||||||
public AdsMediaSource(
|
public AdsMediaSource(
|
||||||
@ -234,6 +234,11 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
return contentMediaSource.getMediaItem();
|
return contentMediaSource.getMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the ads ID this source is serving. */
|
||||||
|
public Object getAdsId() {
|
||||||
|
return adsId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
return Util.areEqual(getAdsConfiguration(getMediaItem()), getAdsConfiguration(mediaItem))
|
return Util.areEqual(getAdsConfiguration(getMediaItem()), getAdsConfiguration(mediaItem))
|
||||||
@ -330,7 +335,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
} else {
|
} else {
|
||||||
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
|
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
|
||||||
contentTimeline = newTimeline;
|
contentTimeline = newTimeline;
|
||||||
mainHandler.post(() -> adsLoader.handleContentTimelineChanged(getMediaItem(), newTimeline));
|
mainHandler.post(() -> adsLoader.handleContentTimelineChanged(this, newTimeline));
|
||||||
}
|
}
|
||||||
maybeUpdateSourceInfo();
|
maybeUpdateSourceInfo();
|
||||||
}
|
}
|
||||||
|
@ -387,7 +387,8 @@ public final class AdsMediaSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
|
public void handleContentTimelineChanged(
|
||||||
|
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
contentTimelineChangedCalledLatch.countDown();
|
contentTimelineChangedCalledLatch.countDown();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -563,7 +564,8 @@ public final class AdsMediaSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {
|
public void handleContentTimelineChanged(
|
||||||
|
AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
contentTimelineChangedCallCount.incrementAndGet();
|
contentTimelineChangedCallCount.incrementAndGet();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -724,7 +726,7 @@ public final class AdsMediaSourceTest {
|
|||||||
IOException exception) {}
|
IOException exception) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleContentTimelineChanged(MediaItem mediaItem, Timeline timeline) {}
|
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
||||||
|
@ -0,0 +1,559 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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 androidx.media3.exoplayer.hls;
|
||||||
|
|
||||||
|
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.AdPlaybackState;
|
||||||
|
import androidx.media3.common.AdViewProvider;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MediaItem.LocalConfiguration;
|
||||||
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.Timeline.Period;
|
||||||
|
import androidx.media3.common.Timeline.Window;
|
||||||
|
import androidx.media3.common.util.Consumer;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.datasource.DataSpec;
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial;
|
||||||
|
import androidx.media3.exoplayer.source.ads.AdsLoader;
|
||||||
|
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@linkplain AdsLoader ads loader} that reads interstitials from the HLS playlist, adds them to
|
||||||
|
* the {@link AdPlaybackState} and passes the ad playback state to {@link
|
||||||
|
* EventListener#onAdPlaybackState(AdPlaybackState)}.
|
||||||
|
*
|
||||||
|
* <p>An ads ID must be unique within the playlist of ExoPlayer. If this is the case, a single
|
||||||
|
* {@link HlsInterstitialsAdsLoader} instance can be passed to multiple {@linkplain AdsMediaSource
|
||||||
|
* ads media sources}. These ad media source can be added to the same playlist as far as each of the
|
||||||
|
* sources have a different ads IDs.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||||
|
|
||||||
|
/** A listener to be notified of events emitted by the ads loader. */
|
||||||
|
public interface Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the ads loader was started for the given HLS media item and ads ID.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adViewProvider {@linkplain AdViewProvider Provider} of views for the ad UI.
|
||||||
|
*/
|
||||||
|
default void onStart(MediaItem mediaItem, Object adsId, AdViewProvider adViewProvider) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the timeline of the content media source has changed. The {@link HlsManifest} of
|
||||||
|
* the content source can be accessed through {@link Window#manifest}.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param hlsContentTimeline The latest {@link Timeline}.
|
||||||
|
*/
|
||||||
|
default void onContentTimelineChanged(
|
||||||
|
MediaItem mediaItem, Object adsId, Timeline hlsContentTimeline) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when preparation of an ad period has completed successfully.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adGroupIndex The index of the ad group in the ad media source.
|
||||||
|
* @param adIndexInAdGroup The index of the ad in the ad group.
|
||||||
|
*/
|
||||||
|
default void onPrepareCompleted(
|
||||||
|
MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when preparation of an ad period failed.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adGroupIndex The index of the ad group in the ad media source.
|
||||||
|
* @param adIndexInAdGroup The index of the ad in the ad group.
|
||||||
|
* @param exception The {@link IOException} thrown when preparing.
|
||||||
|
*/
|
||||||
|
default void onPrepareError(
|
||||||
|
MediaItem mediaItem,
|
||||||
|
Object adsId,
|
||||||
|
int adGroupIndex,
|
||||||
|
int adIndexInAdGroup,
|
||||||
|
IOException exception) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when {@link Metadata} is emitted by the player during an ad period of an active HLS
|
||||||
|
* media item.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adGroupIndex The index of the ad group in the ad media source.
|
||||||
|
* @param adIndexInAdGroup The index of the ad in the ad group.
|
||||||
|
* @param metadata The emitted {@link Metadata}.
|
||||||
|
*/
|
||||||
|
default void onMetadata(
|
||||||
|
MediaItem mediaItem,
|
||||||
|
Object adsId,
|
||||||
|
int adGroupIndex,
|
||||||
|
int adIndexInAdGroup,
|
||||||
|
Metadata metadata) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an ad period has completed playback and transitioned to the following ad or
|
||||||
|
* content period, or the playlist ended.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adGroupIndex The index of the ad group in the ad media source.
|
||||||
|
* @param adIndexInAdGroup The index of the ad in the ad group.
|
||||||
|
*/
|
||||||
|
default void onAdCompleted(
|
||||||
|
MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the ads loader was stopped for the given HLS media item.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} of the content media source.
|
||||||
|
* @param adsId The ads ID of the ads media source.
|
||||||
|
* @param adPlaybackState The {@link AdPlaybackState} after the ad media source was released.
|
||||||
|
*/
|
||||||
|
default void onStop(MediaItem mediaItem, Object adsId, AdPlaybackState adPlaybackState) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "HlsInterstitiaAdsLoader";
|
||||||
|
|
||||||
|
private final PlayerListener playerListener;
|
||||||
|
private final Map<Object, EventListener> activeEventListeners;
|
||||||
|
private final Map<Object, AdPlaybackState> activeAdPlaybackStates;
|
||||||
|
private final List<Listener> listeners;
|
||||||
|
private final Set<Object> unsupportedAdsIds;
|
||||||
|
|
||||||
|
@Nullable private Player player;
|
||||||
|
private boolean isReleased;
|
||||||
|
|
||||||
|
/** Creates an instance. */
|
||||||
|
public HlsInterstitialsAdsLoader() {
|
||||||
|
playerListener = new PlayerListener();
|
||||||
|
activeEventListeners = new HashMap<>();
|
||||||
|
activeAdPlaybackStates = new HashMap<>();
|
||||||
|
listeners = new ArrayList<>();
|
||||||
|
unsupportedAdsIds = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a {@link Listener}. */
|
||||||
|
public void addListener(Listener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes a {@link Listener}. */
|
||||||
|
public void removeListener(Listener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation of AdsLoader methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException If an app is attempting to set a new player instance after {@link
|
||||||
|
* #release} was called or while {@linkplain AdsMediaSource ads media sources} started by the
|
||||||
|
* old player are still active, an {@link IllegalStateException} is thrown. Release the old
|
||||||
|
* player first, or remove all ads media sources from it before setting another player
|
||||||
|
* instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setPlayer(@Nullable Player player) {
|
||||||
|
checkState(!isReleased);
|
||||||
|
if (Objects.equals(this.player, player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.player != null && !activeEventListeners.isEmpty()) {
|
||||||
|
this.player.removeListener(playerListener);
|
||||||
|
}
|
||||||
|
checkState(player == null || activeEventListeners.isEmpty());
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
|
||||||
|
for (int contentType : contentTypes) {
|
||||||
|
if (contentType == C.CONTENT_TYPE_HLS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(
|
||||||
|
AdsMediaSource adsMediaSource,
|
||||||
|
DataSpec adTagDataSpec,
|
||||||
|
Object adsId,
|
||||||
|
AdViewProvider adViewProvider,
|
||||||
|
EventListener eventListener) {
|
||||||
|
if (isReleased) {
|
||||||
|
// Run without ads after release to not interrupt playback.
|
||||||
|
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeAdPlaybackStates.containsKey(adsId) || unsupportedAdsIds.contains(adsId)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"media item with adsId='"
|
||||||
|
+ adsId
|
||||||
|
+ "' already started. Make sure adsIds are unique within the same playlist.");
|
||||||
|
}
|
||||||
|
if (activeEventListeners.isEmpty()) {
|
||||||
|
// Set the player listener when the first ad starts.
|
||||||
|
checkStateNotNull(player, "setPlayer(Player) needs to be called").addListener(playerListener);
|
||||||
|
}
|
||||||
|
activeEventListeners.put(adsId, eventListener);
|
||||||
|
MediaItem mediaItem = adsMediaSource.getMediaItem();
|
||||||
|
if (player != null && isSupportedMediaItem(mediaItem, player.getCurrentTimeline())) {
|
||||||
|
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
|
||||||
|
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
|
||||||
|
notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider));
|
||||||
|
} else {
|
||||||
|
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
|
||||||
|
if (player != null) {
|
||||||
|
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
|
||||||
|
unsupportedAdsIds.add(adsId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
|
||||||
|
Object adsId = adsMediaSource.getAdsId();
|
||||||
|
if (isReleased) {
|
||||||
|
EventListener eventListener = activeEventListeners.remove(adsId);
|
||||||
|
if (eventListener != null) {
|
||||||
|
unsupportedAdsIds.remove(adsId);
|
||||||
|
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.remove(adsId));
|
||||||
|
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
|
||||||
|
// Play without ads after release to not interrupt playback.
|
||||||
|
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
|
||||||
|
if (!adPlaybackState.equals(AdPlaybackState.NONE)) {
|
||||||
|
// VOD only. Updating the playback state is not supported yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adPlaybackState = new AdPlaybackState(adsId);
|
||||||
|
Window window = timeline.getWindow(0, new Window());
|
||||||
|
if (window.manifest instanceof HlsManifest) {
|
||||||
|
adPlaybackState =
|
||||||
|
mapHlsInterstitialsToAdPlaybackState(
|
||||||
|
((HlsManifest) window.manifest).mediaPlaylist, adPlaybackState);
|
||||||
|
}
|
||||||
|
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||||
|
if (!unsupportedAdsIds.contains(adsId)) {
|
||||||
|
notifyListeners(
|
||||||
|
listener ->
|
||||||
|
listener.onContentTimelineChanged(adsMediaSource.getMediaItem(), adsId, timeline));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handlePrepareComplete(
|
||||||
|
AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) {
|
||||||
|
Object adsId = adsMediaSource.getAdsId();
|
||||||
|
if (!isReleased && !unsupportedAdsIds.contains(adsId)) {
|
||||||
|
notifyListeners(
|
||||||
|
listener ->
|
||||||
|
listener.onPrepareCompleted(
|
||||||
|
adsMediaSource.getMediaItem(), adsId, adGroupIndex, adIndexInAdGroup));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handlePrepareError(
|
||||||
|
AdsMediaSource adsMediaSource,
|
||||||
|
int adGroupIndex,
|
||||||
|
int adIndexInAdGroup,
|
||||||
|
IOException exception) {
|
||||||
|
Object adsId = adsMediaSource.getAdsId();
|
||||||
|
AdPlaybackState adPlaybackState =
|
||||||
|
checkNotNull(activeAdPlaybackStates.get(adsId))
|
||||||
|
.withAdLoadError(adGroupIndex, adIndexInAdGroup);
|
||||||
|
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||||
|
if (!isReleased && !unsupportedAdsIds.contains(adsId)) {
|
||||||
|
notifyListeners(
|
||||||
|
listener ->
|
||||||
|
listener.onPrepareError(
|
||||||
|
adsMediaSource.getMediaItem(), adsId, adGroupIndex, adIndexInAdGroup, exception));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {
|
||||||
|
Object adsId = adsMediaSource.getAdsId();
|
||||||
|
activeEventListeners.remove(adsId);
|
||||||
|
@Nullable AdPlaybackState adPlaybackState = activeAdPlaybackStates.remove(adsId);
|
||||||
|
if (player != null && activeEventListeners.isEmpty()) {
|
||||||
|
player.removeListener(playerListener);
|
||||||
|
if (isReleased) {
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isReleased && !unsupportedAdsIds.contains(adsId)) {
|
||||||
|
notifyListeners(
|
||||||
|
listener ->
|
||||||
|
listener.onStop(
|
||||||
|
adsMediaSource.getMediaItem(),
|
||||||
|
adsMediaSource.getAdsId(),
|
||||||
|
checkNotNull(adPlaybackState)));
|
||||||
|
}
|
||||||
|
unsupportedAdsIds.remove(adsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
// Note: Do not clear active resources as media sources still may have references to the loader
|
||||||
|
// and we need to ensure sources can complete playback.
|
||||||
|
if (activeEventListeners.isEmpty()) {
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
isReleased = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// private methods
|
||||||
|
|
||||||
|
private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
||||||
|
@Nullable
|
||||||
|
AdPlaybackState oldAdPlaybackState = activeAdPlaybackStates.put(adsId, adPlaybackState);
|
||||||
|
if (!adPlaybackState.equals(oldAdPlaybackState)) {
|
||||||
|
@Nullable EventListener eventListener = activeEventListeners.get(adsId);
|
||||||
|
if (eventListener != null) {
|
||||||
|
eventListener.onAdPlaybackState(adPlaybackState);
|
||||||
|
} else {
|
||||||
|
activeAdPlaybackStates.remove(adsId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners(Consumer<Listener> callable) {
|
||||||
|
for (int i = 0; i < listeners.size(); i++) {
|
||||||
|
callable.accept(listeners.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupportedMediaItem(MediaItem mediaItem, Timeline timeline) {
|
||||||
|
return isHlsMediaItem(mediaItem) && !isLiveMediaItem(mediaItem, timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLiveMediaItem(MediaItem mediaItem, Timeline timeline) {
|
||||||
|
int windowIndex = timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
|
||||||
|
Window window = new Window();
|
||||||
|
while (windowIndex != C.INDEX_UNSET) {
|
||||||
|
timeline.getWindow(windowIndex, window);
|
||||||
|
if (window.mediaItem.equals(mediaItem)) {
|
||||||
|
return window.isLive();
|
||||||
|
}
|
||||||
|
windowIndex =
|
||||||
|
timeline.getNextWindowIndex(
|
||||||
|
windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHlsMediaItem(MediaItem mediaItem) {
|
||||||
|
LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration);
|
||||||
|
return Objects.equals(localConfiguration.mimeType, MimeTypes.APPLICATION_M3U8)
|
||||||
|
|| Util.inferContentType(localConfiguration.uri) == C.CONTENT_TYPE_HLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdPlaybackState mapHlsInterstitialsToAdPlaybackState(
|
||||||
|
HlsMediaPlaylist hlsMediaPlaylist, AdPlaybackState adPlaybackState) {
|
||||||
|
for (int i = 0; i < hlsMediaPlaylist.interstitials.size(); i++) {
|
||||||
|
Interstitial interstitial = hlsMediaPlaylist.interstitials.get(i);
|
||||||
|
if (interstitial.assetUri == null) {
|
||||||
|
Log.w(TAG, "Ignoring interstitials with X-ASSET-LIST. Not yet supported.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long positionUs;
|
||||||
|
if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_PRE)) {
|
||||||
|
positionUs = 0;
|
||||||
|
} else if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_POST)) {
|
||||||
|
positionUs = C.TIME_END_OF_SOURCE;
|
||||||
|
} else {
|
||||||
|
positionUs = interstitial.startDateUnixUs - hlsMediaPlaylist.startTimeUs;
|
||||||
|
}
|
||||||
|
// Check whether and at which index to insert an ad group for the interstitial start time.
|
||||||
|
int adGroupIndex =
|
||||||
|
adPlaybackState.getAdGroupIndexForPositionUs(
|
||||||
|
positionUs, /* periodDurationUs= */ hlsMediaPlaylist.durationUs);
|
||||||
|
if (adGroupIndex == C.INDEX_UNSET) {
|
||||||
|
// There is no ad group before or at the interstitials position.
|
||||||
|
adGroupIndex = 0;
|
||||||
|
adPlaybackState = adPlaybackState.withNewAdGroup(0, positionUs);
|
||||||
|
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != positionUs) {
|
||||||
|
// There is an ad group before the interstitials. Insert after that index.
|
||||||
|
adGroupIndex++;
|
||||||
|
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int adIndexInAdGroup = max(adPlaybackState.getAdGroup(adGroupIndex).count, 0);
|
||||||
|
|
||||||
|
// Insert duration of new interstitial into existing ad durations.
|
||||||
|
long interstitialDurationUs =
|
||||||
|
getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
|
||||||
|
long[] adDurations;
|
||||||
|
if (adIndexInAdGroup == 0) {
|
||||||
|
adDurations = new long[1];
|
||||||
|
} else {
|
||||||
|
long[] previousDurations = adPlaybackState.getAdGroup(adGroupIndex).durationsUs;
|
||||||
|
adDurations = new long[previousDurations.length + 1];
|
||||||
|
System.arraycopy(previousDurations, 0, adDurations, 0, previousDurations.length);
|
||||||
|
}
|
||||||
|
adDurations[adDurations.length - 1] = interstitialDurationUs;
|
||||||
|
|
||||||
|
long resumeOffsetIncrementUs =
|
||||||
|
interstitial.resumeOffsetUs != C.TIME_UNSET
|
||||||
|
? interstitial.resumeOffsetUs
|
||||||
|
: (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L);
|
||||||
|
long resumeOffsetUs =
|
||||||
|
adPlaybackState.getAdGroup(adGroupIndex).contentResumeOffsetUs + resumeOffsetIncrementUs;
|
||||||
|
adPlaybackState =
|
||||||
|
adPlaybackState
|
||||||
|
.withAdCount(adGroupIndex, /* adCount= */ adIndexInAdGroup + 1)
|
||||||
|
.withAdDurationsUs(adGroupIndex, adDurations)
|
||||||
|
.withContentResumeOffsetUs(adGroupIndex, resumeOffsetUs)
|
||||||
|
.withAvailableAdMediaItem(
|
||||||
|
adGroupIndex, adIndexInAdGroup, MediaItem.fromUri(interstitial.assetUri));
|
||||||
|
}
|
||||||
|
return adPlaybackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getInterstitialDurationUs(Interstitial interstitial, long defaultDurationUs) {
|
||||||
|
if (interstitial.playoutLimitUs != C.TIME_UNSET) {
|
||||||
|
return interstitial.playoutLimitUs;
|
||||||
|
} else if (interstitial.durationUs != C.TIME_UNSET) {
|
||||||
|
return interstitial.durationUs;
|
||||||
|
} else if (interstitial.endDateUnixUs != C.TIME_UNSET) {
|
||||||
|
return interstitial.endDateUnixUs - interstitial.startDateUnixUs;
|
||||||
|
} else if (interstitial.plannedDurationUs != C.TIME_UNSET) {
|
||||||
|
return interstitial.plannedDurationUs;
|
||||||
|
}
|
||||||
|
return defaultDurationUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PlayerListener implements Player.Listener {
|
||||||
|
|
||||||
|
private final Period period = new Period();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadata(Metadata metadata) {
|
||||||
|
@Nullable Player player = HlsInterstitialsAdsLoader.this.player;
|
||||||
|
if (player == null || !player.isPlayingAd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getCurrentTimeline().getPeriod(player.getCurrentPeriodIndex(), period);
|
||||||
|
@Nullable Object adsId = period.adPlaybackState.adsId;
|
||||||
|
if (adsId == null || !activeAdPlaybackStates.containsKey(adsId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MediaItem currentMediaItem = checkNotNull(player.getCurrentMediaItem());
|
||||||
|
int currentAdGroupIndex = player.getCurrentAdGroupIndex();
|
||||||
|
int currentAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup();
|
||||||
|
notifyListeners(
|
||||||
|
listener ->
|
||||||
|
listener.onMetadata(
|
||||||
|
currentMediaItem, adsId, currentAdGroupIndex, currentAdIndexInAdGroup, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(
|
||||||
|
Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
||||||
|
if (reason != DISCONTINUITY_REASON_AUTO_TRANSITION
|
||||||
|
|| player == null
|
||||||
|
|| oldPosition.mediaItem == null
|
||||||
|
|| oldPosition.adGroupIndex == C.INDEX_UNSET) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getCurrentTimeline().getPeriod(oldPosition.periodIndex, period);
|
||||||
|
@Nullable Object adsId = period.adPlaybackState.adsId;
|
||||||
|
if (adsId != null && activeAdPlaybackStates.containsKey(adsId)) {
|
||||||
|
markAdAsPlayedAndNotifyListeners(
|
||||||
|
oldPosition.mediaItem, adsId, oldPosition.adGroupIndex, oldPosition.adIndexInAdGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackStateChanged(int playbackState) {
|
||||||
|
Player player = HlsInterstitialsAdsLoader.this.player;
|
||||||
|
if (playbackState != Player.STATE_ENDED || player == null || !player.isPlayingAd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getCurrentTimeline().getPeriod(player.getCurrentPeriodIndex(), period);
|
||||||
|
@Nullable Object adsId = period.adPlaybackState.adsId;
|
||||||
|
if (adsId != null && activeAdPlaybackStates.containsKey(adsId)) {
|
||||||
|
markAdAsPlayedAndNotifyListeners(
|
||||||
|
checkNotNull(player.getCurrentMediaItem()),
|
||||||
|
adsId,
|
||||||
|
player.getCurrentAdGroupIndex(),
|
||||||
|
player.getCurrentAdIndexInAdGroup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAdAsPlayedAndNotifyListeners(
|
||||||
|
MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||||
|
@Nullable AdPlaybackState adPlaybackState = activeAdPlaybackStates.get(adsId);
|
||||||
|
if (adPlaybackState != null) {
|
||||||
|
adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup);
|
||||||
|
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onAdCompleted(mediaItem, adsId, adGroupIndex, adIndexInAdGroup));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user