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
5c2806ec
authored
Nov 06, 2019
by
bachinger
Committed by
Oliver Woodman
Nov 15, 2019
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Playlist API: add Playlist and PlaylistTest
PiperOrigin-RevId: 278875587
parent
cd2c1f2f
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
1222 additions
and
5 deletions
library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java → library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java
library/core/src/main/java/com/google/android/exoplayer2/Playlist.java
library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java
library/core/src/main/java/com/google/android/exoplayer2/
source/
AbstractConcatenatedTimeline.java
→
library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java
View file @
5c2806ec
...
...
@@ -13,16 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
source
;
package
com
.
google
.
android
.
exoplayer2
;
import
android.util.Pair
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Player
;
import
com.google.android.exoplayer2.Timeline
;
import
com.google.android.exoplayer2.source.ShuffleOrder
;
import
com.google.android.exoplayer2.util.Assertions
;
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
/* package */
abstract
class
AbstractConcatenatedTimeline
extends
Timeline
{
public
abstract
class
AbstractConcatenatedTimeline
extends
Timeline
{
private
final
int
childCount
;
private
final
ShuffleOrder
shuffleOrder
;
...
...
library/core/src/main/java/com/google/android/exoplayer2/Playlist.java
0 → 100644
View file @
5c2806ec
/*
* Copyright (C) 2019 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
;
import
android.os.Handler
;
import
androidx.annotation.Nullable
;
import
com.google.android.exoplayer2.analytics.AnalyticsCollector
;
import
com.google.android.exoplayer2.source.MaskingMediaPeriod
;
import
com.google.android.exoplayer2.source.MaskingMediaSource
;
import
com.google.android.exoplayer2.source.MediaPeriod
;
import
com.google.android.exoplayer2.source.MediaSource
;
import
com.google.android.exoplayer2.source.MediaSourceEventListener
;
import
com.google.android.exoplayer2.source.ShuffleOrder
;
import
com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder
;
import
com.google.android.exoplayer2.upstream.Allocator
;
import
com.google.android.exoplayer2.upstream.TransferListener
;
import
com.google.android.exoplayer2.util.Assertions
;
import
com.google.android.exoplayer2.util.Util
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.Collection
;
import
java.util.HashMap
;
import
java.util.HashSet
;
import
java.util.IdentityHashMap
;
import
java.util.Iterator
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
/**
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
* once in the playlist.
*
* <p>With the exception of the constructor, all methods are called on the playback thread.
*/
/* package */
class
Playlist
{
/** Listener for source events. */
public
interface
PlaylistInfoRefreshListener
{
/**
* Called when the timeline of a media item has changed and a new timeline that reflects the
* current playlist state needs to be created by calling {@link #createTimeline()}.
*
* <p>Called on the playback thread.
*/
void
onPlaylistUpdateRequested
();
}
private
final
List
<
MediaSourceHolder
>
mediaSourceHolders
;
private
final
Map
<
MediaPeriod
,
MediaSourceHolder
>
mediaSourceByMediaPeriod
;
private
final
Map
<
Object
,
MediaSourceHolder
>
mediaSourceByUid
;
private
final
PlaylistInfoRefreshListener
playlistInfoListener
;
private
final
MediaSourceEventListener
.
EventDispatcher
eventDispatcher
;
private
final
HashMap
<
Playlist
.
MediaSourceHolder
,
MediaSourceAndListener
>
childSources
;
private
final
Set
<
MediaSourceHolder
>
enabledMediaSourceHolders
;
private
ShuffleOrder
shuffleOrder
;
private
boolean
isPrepared
;
@Nullable
private
TransferListener
mediaTransferListener
;
@SuppressWarnings
(
"initialization"
)
public
Playlist
(
PlaylistInfoRefreshListener
listener
)
{
playlistInfoListener
=
listener
;
shuffleOrder
=
new
DefaultShuffleOrder
(
0
);
mediaSourceByMediaPeriod
=
new
IdentityHashMap
<>();
mediaSourceByUid
=
new
HashMap
<>();
mediaSourceHolders
=
new
ArrayList
<>();
eventDispatcher
=
new
MediaSourceEventListener
.
EventDispatcher
();
childSources
=
new
HashMap
<>();
enabledMediaSourceHolders
=
new
HashSet
<>();
}
/**
* Sets the media sources replacing any sources previously contained in the playlist.
*
* @param holders The list of {@link MediaSourceHolder}s to set.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
*/
public
final
Timeline
setMediaSources
(
List
<
MediaSourceHolder
>
holders
,
ShuffleOrder
shuffleOrder
)
{
removeMediaSourcesInternal
(
/* fromIndex= */
0
,
/* toIndex= */
mediaSourceHolders
.
size
());
return
addMediaSources
(
/* index= */
this
.
mediaSourceHolders
.
size
(),
holders
,
shuffleOrder
);
}
/**
* Adds multiple {@link MediaSourceHolder}s to the playlist.
*
* @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
* must be in the range of 0 <= index <= {@link #getSize()}.
* @param holders A list of {@link MediaSourceHolder}s to be added.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
*/
public
final
Timeline
addMediaSources
(
int
index
,
List
<
MediaSourceHolder
>
holders
,
ShuffleOrder
shuffleOrder
)
{
if
(!
holders
.
isEmpty
())
{
this
.
shuffleOrder
=
shuffleOrder
;
for
(
int
insertionIndex
=
index
;
insertionIndex
<
index
+
holders
.
size
();
insertionIndex
++)
{
MediaSourceHolder
holder
=
holders
.
get
(
insertionIndex
-
index
);
if
(
insertionIndex
>
0
)
{
MediaSourceHolder
previousHolder
=
mediaSourceHolders
.
get
(
insertionIndex
-
1
);
Timeline
previousTimeline
=
previousHolder
.
mediaSource
.
getTimeline
();
holder
.
reset
(
/* firstWindowInChildIndex= */
previousHolder
.
firstWindowIndexInChild
+
previousTimeline
.
getWindowCount
());
}
else
{
holder
.
reset
(
/* firstWindowIndexInChild= */
0
);
}
Timeline
newTimeline
=
holder
.
mediaSource
.
getTimeline
();
correctOffsets
(
/* startIndex= */
insertionIndex
,
/* windowOffsetUpdate= */
newTimeline
.
getWindowCount
());
mediaSourceHolders
.
add
(
insertionIndex
,
holder
);
mediaSourceByUid
.
put
(
holder
.
uid
,
holder
);
if
(
isPrepared
)
{
prepareChildSource
(
holder
);
if
(
mediaSourceByMediaPeriod
.
isEmpty
())
{
enabledMediaSourceHolders
.
add
(
holder
);
}
else
{
disableChildSource
(
holder
);
}
}
}
}
return
createTimeline
();
}
/**
* Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index
* (included) and a final index (excluded).
*
* <p>Note: when specified range is empty, no actual media source is removed and no exception is
* thrown.
*
* @param fromIndex The initial range index, pointing to the first media source that will be
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
* @param toIndex The final range index, pointing to the first media source that will be left
* untouched. This index must be in the range of 0 <= index <= {@link #getSize()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
*/
public
final
Timeline
removeMediaSourceRange
(
int
fromIndex
,
int
toIndex
,
ShuffleOrder
shuffleOrder
)
{
Assertions
.
checkArgument
(
fromIndex
>=
0
&&
fromIndex
<=
toIndex
&&
toIndex
<=
getSize
());
this
.
shuffleOrder
=
shuffleOrder
;
removeMediaSourcesInternal
(
fromIndex
,
toIndex
);
return
createTimeline
();
}
/**
* Moves an existing media source within the playlist.
*
* @param currentIndex The current index of the media source in the playlist. This index must be
* in the range of 0 <= index < {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 <= index < {@link #getSize()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0,
* {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0
*/
public
final
Timeline
moveMediaSource
(
int
currentIndex
,
int
newIndex
,
ShuffleOrder
shuffleOrder
)
{
return
moveMediaSourceRange
(
currentIndex
,
currentIndex
+
1
,
newIndex
,
shuffleOrder
);
}
/**
* Moves a range of media sources within the playlist.
*
* <p>Note: when specified range is empty or the from index equals the new from index, no actual
* media source is moved and no exception is thrown.
*
* @param fromIndex The initial range index, pointing to the first media source of the range that
* will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}.
* @param toIndex The final range index, pointing to the first media source that will be left
* untouched. This index must be larger or equals than {@code fromIndex}.
* @param newFromIndex The target index of the first media source of the range that will be moved.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
* {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code
* newFromIndex} < 0
*/
public
Timeline
moveMediaSourceRange
(
int
fromIndex
,
int
toIndex
,
int
newFromIndex
,
ShuffleOrder
shuffleOrder
)
{
Assertions
.
checkArgument
(
fromIndex
>=
0
&&
fromIndex
<=
toIndex
&&
toIndex
<=
getSize
()
&&
newFromIndex
>=
0
);
this
.
shuffleOrder
=
shuffleOrder
;
if
(
fromIndex
==
toIndex
||
fromIndex
==
newFromIndex
)
{
return
createTimeline
();
}
int
startIndex
=
Math
.
min
(
fromIndex
,
newFromIndex
);
int
newEndIndex
=
newFromIndex
+
(
toIndex
-
fromIndex
)
-
1
;
int
endIndex
=
Math
.
max
(
newEndIndex
,
toIndex
-
1
);
int
windowOffset
=
mediaSourceHolders
.
get
(
startIndex
).
firstWindowIndexInChild
;
moveMediaSourceHolders
(
mediaSourceHolders
,
fromIndex
,
toIndex
,
newFromIndex
);
for
(
int
i
=
startIndex
;
i
<=
endIndex
;
i
++)
{
MediaSourceHolder
holder
=
mediaSourceHolders
.
get
(
i
);
holder
.
firstWindowIndexInChild
=
windowOffset
;
windowOffset
+=
holder
.
mediaSource
.
getTimeline
().
getWindowCount
();
}
return
createTimeline
();
}
/** Clears the playlist. */
public
final
Timeline
clear
(
@Nullable
ShuffleOrder
shuffleOrder
)
{
this
.
shuffleOrder
=
shuffleOrder
!=
null
?
shuffleOrder
:
this
.
shuffleOrder
.
cloneAndClear
();
removeMediaSourcesInternal
(
/* fromIndex= */
0
,
/* toIndex= */
getSize
());
return
createTimeline
();
}
/** Whether the playlist is prepared. */
public
final
boolean
isPrepared
()
{
return
isPrepared
;
}
/** Returns the number of media sources in the playlist. */
public
final
int
getSize
()
{
return
mediaSourceHolders
.
size
();
}
/**
* Sets the {@link AnalyticsCollector}.
*
* @param handler The handler on which to call the collector.
* @param analyticsCollector The analytics collector.
*/
public
final
void
setAnalyticsCollector
(
Handler
handler
,
AnalyticsCollector
analyticsCollector
)
{
eventDispatcher
.
addEventListener
(
handler
,
analyticsCollector
);
}
/**
* Sets a new shuffle order to use when shuffling the child media sources.
*
* @param shuffleOrder A {@link ShuffleOrder}.
*/
public
final
Timeline
setShuffleOrder
(
ShuffleOrder
shuffleOrder
)
{
int
size
=
getSize
();
if
(
shuffleOrder
.
getLength
()
!=
size
)
{
shuffleOrder
=
shuffleOrder
.
cloneAndClear
()
.
cloneAndInsert
(
/* insertionIndex= */
0
,
/* insertionCount= */
size
);
}
this
.
shuffleOrder
=
shuffleOrder
;
return
createTimeline
();
}
/** Prepares the playlist. */
public
final
void
prepare
(
@Nullable
TransferListener
mediaTransferListener
)
{
Assertions
.
checkState
(!
isPrepared
);
this
.
mediaTransferListener
=
mediaTransferListener
;
for
(
int
i
=
0
;
i
<
mediaSourceHolders
.
size
();
i
++)
{
MediaSourceHolder
mediaSourceHolder
=
mediaSourceHolders
.
get
(
i
);
prepareChildSource
(
mediaSourceHolder
);
enabledMediaSourceHolders
.
add
(
mediaSourceHolder
);
}
isPrepared
=
true
;
}
/**
* Returns a new {@link MediaPeriod} identified by {@code periodId}.
*
* @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param startPositionUs The expected start position, in microseconds.
* @return A new {@link MediaPeriod}.
*/
public
MediaPeriod
createPeriod
(
MediaSource
.
MediaPeriodId
id
,
Allocator
allocator
,
long
startPositionUs
)
{
Object
mediaSourceHolderUid
=
getMediaSourceHolderUid
(
id
.
periodUid
);
MediaSource
.
MediaPeriodId
childMediaPeriodId
=
id
.
copyWithPeriodUid
(
getChildPeriodUid
(
id
.
periodUid
));
MediaSourceHolder
holder
=
Assertions
.
checkNotNull
(
mediaSourceByUid
.
get
(
mediaSourceHolderUid
));
enableMediaSource
(
holder
);
holder
.
activeMediaPeriodIds
.
add
(
childMediaPeriodId
);
MediaPeriod
mediaPeriod
=
holder
.
mediaSource
.
createPeriod
(
childMediaPeriodId
,
allocator
,
startPositionUs
);
mediaSourceByMediaPeriod
.
put
(
mediaPeriod
,
holder
);
disableUnusedMediaSources
();
return
mediaPeriod
;
}
/**
* Releases the period.
*
* @param mediaPeriod The period to release.
*/
public
final
void
releasePeriod
(
MediaPeriod
mediaPeriod
)
{
MediaSourceHolder
holder
=
Assertions
.
checkNotNull
(
mediaSourceByMediaPeriod
.
remove
(
mediaPeriod
));
holder
.
mediaSource
.
releasePeriod
(
mediaPeriod
);
holder
.
activeMediaPeriodIds
.
remove
(((
MaskingMediaPeriod
)
mediaPeriod
).
id
);
if
(!
mediaSourceByMediaPeriod
.
isEmpty
())
{
disableUnusedMediaSources
();
}
maybeReleaseChildSource
(
holder
);
}
/** Releases the playlist. */
public
final
void
release
()
{
for
(
MediaSourceAndListener
childSource
:
childSources
.
values
())
{
childSource
.
mediaSource
.
releaseSource
(
childSource
.
caller
);
childSource
.
mediaSource
.
removeEventListener
(
childSource
.
eventListener
);
}
childSources
.
clear
();
enabledMediaSourceHolders
.
clear
();
isPrepared
=
false
;
}
/** Throws any pending error encountered while loading or refreshing. */
public
final
void
maybeThrowSourceInfoRefreshError
()
throws
IOException
{
for
(
MediaSourceAndListener
childSource
:
childSources
.
values
())
{
childSource
.
mediaSource
.
maybeThrowSourceInfoRefreshError
();
}
}
/** Creates a timeline reflecting the current state of the playlist. */
public
final
Timeline
createTimeline
()
{
if
(
mediaSourceHolders
.
isEmpty
())
{
return
Timeline
.
EMPTY
;
}
int
windowOffset
=
0
;
for
(
int
i
=
0
;
i
<
mediaSourceHolders
.
size
();
i
++)
{
MediaSourceHolder
mediaSourceHolder
=
mediaSourceHolders
.
get
(
i
);
mediaSourceHolder
.
firstWindowIndexInChild
=
windowOffset
;
windowOffset
+=
mediaSourceHolder
.
mediaSource
.
getTimeline
().
getWindowCount
();
}
return
new
PlaylistTimeline
(
mediaSourceHolders
,
shuffleOrder
);
}
// Internal methods.
private
void
enableMediaSource
(
MediaSourceHolder
mediaSourceHolder
)
{
enabledMediaSourceHolders
.
add
(
mediaSourceHolder
);
@Nullable
MediaSourceAndListener
enabledChild
=
childSources
.
get
(
mediaSourceHolder
);
if
(
enabledChild
!=
null
)
{
enabledChild
.
mediaSource
.
enable
(
enabledChild
.
caller
);
}
}
private
void
disableUnusedMediaSources
()
{
Iterator
<
MediaSourceHolder
>
iterator
=
enabledMediaSourceHolders
.
iterator
();
while
(
iterator
.
hasNext
())
{
MediaSourceHolder
holder
=
iterator
.
next
();
if
(
holder
.
activeMediaPeriodIds
.
isEmpty
())
{
disableChildSource
(
holder
);
iterator
.
remove
();
}
}
}
private
void
disableChildSource
(
MediaSourceHolder
holder
)
{
@Nullable
MediaSourceAndListener
disabledChild
=
childSources
.
get
(
holder
);
if
(
disabledChild
!=
null
)
{
disabledChild
.
mediaSource
.
disable
(
disabledChild
.
caller
);
}
}
private
void
removeMediaSourcesInternal
(
int
fromIndex
,
int
toIndex
)
{
for
(
int
index
=
toIndex
-
1
;
index
>=
fromIndex
;
index
--)
{
MediaSourceHolder
holder
=
mediaSourceHolders
.
remove
(
index
);
mediaSourceByUid
.
remove
(
holder
.
uid
);
Timeline
oldTimeline
=
holder
.
mediaSource
.
getTimeline
();
correctOffsets
(
/* startIndex= */
index
,
/* windowOffsetUpdate= */
-
oldTimeline
.
getWindowCount
());
holder
.
isRemoved
=
true
;
if
(
isPrepared
)
{
maybeReleaseChildSource
(
holder
);
}
}
}
private
void
correctOffsets
(
int
startIndex
,
int
windowOffsetUpdate
)
{
for
(
int
i
=
startIndex
;
i
<
mediaSourceHolders
.
size
();
i
++)
{
MediaSourceHolder
mediaSourceHolder
=
mediaSourceHolders
.
get
(
i
);
mediaSourceHolder
.
firstWindowIndexInChild
+=
windowOffsetUpdate
;
}
}
// Internal methods to manage child sources.
@Nullable
private
static
MediaSource
.
MediaPeriodId
getMediaPeriodIdForChildMediaPeriodId
(
MediaSourceHolder
mediaSourceHolder
,
MediaSource
.
MediaPeriodId
mediaPeriodId
)
{
for
(
int
i
=
0
;
i
<
mediaSourceHolder
.
activeMediaPeriodIds
.
size
();
i
++)
{
// Ensure the reported media period id has the same window sequence number as the one created
// by this media source. Otherwise it does not belong to this child source.
if
(
mediaSourceHolder
.
activeMediaPeriodIds
.
get
(
i
).
windowSequenceNumber
==
mediaPeriodId
.
windowSequenceNumber
)
{
Object
periodUid
=
getPeriodUid
(
mediaSourceHolder
,
mediaPeriodId
.
periodUid
);
return
mediaPeriodId
.
copyWithPeriodUid
(
periodUid
);
}
}
return
null
;
}
private
static
int
getWindowIndexForChildWindowIndex
(
MediaSourceHolder
mediaSourceHolder
,
int
windowIndex
)
{
return
windowIndex
+
mediaSourceHolder
.
firstWindowIndexInChild
;
}
private
void
prepareChildSource
(
MediaSourceHolder
holder
)
{
MediaSource
mediaSource
=
holder
.
mediaSource
;
MediaSource
.
MediaSourceCaller
caller
=
(
source
,
timeline
)
->
playlistInfoListener
.
onPlaylistUpdateRequested
();
MediaSourceEventListener
eventListener
=
new
ForwardingEventListener
(
holder
);
childSources
.
put
(
holder
,
new
MediaSourceAndListener
(
mediaSource
,
caller
,
eventListener
));
mediaSource
.
addEventListener
(
new
Handler
(),
eventListener
);
mediaSource
.
prepareSource
(
caller
,
mediaTransferListener
);
}
private
void
maybeReleaseChildSource
(
MediaSourceHolder
mediaSourceHolder
)
{
// Release if the source has been removed from the playlist and no periods are still active.
if
(
mediaSourceHolder
.
isRemoved
&&
mediaSourceHolder
.
activeMediaPeriodIds
.
isEmpty
())
{
MediaSourceAndListener
removedChild
=
Assertions
.
checkNotNull
(
childSources
.
remove
(
mediaSourceHolder
));
removedChild
.
mediaSource
.
releaseSource
(
removedChild
.
caller
);
removedChild
.
mediaSource
.
removeEventListener
(
removedChild
.
eventListener
);
enabledMediaSourceHolders
.
remove
(
mediaSourceHolder
);
}
}
/** Return uid of media source holder from period uid of concatenated source. */
private
static
Object
getMediaSourceHolderUid
(
Object
periodUid
)
{
return
PlaylistTimeline
.
getChildTimelineUidFromConcatenatedUid
(
periodUid
);
}
/** Return uid of child period from period uid of concatenated source. */
private
static
Object
getChildPeriodUid
(
Object
periodUid
)
{
return
PlaylistTimeline
.
getChildPeriodUidFromConcatenatedUid
(
periodUid
);
}
private
static
Object
getPeriodUid
(
MediaSourceHolder
holder
,
Object
childPeriodUid
)
{
return
PlaylistTimeline
.
getConcatenatedUid
(
holder
.
uid
,
childPeriodUid
);
}
/* package */
static
void
moveMediaSourceHolders
(
List
<
MediaSourceHolder
>
mediaSourceHolders
,
int
fromIndex
,
int
toIndex
,
int
newFromIndex
)
{
MediaSourceHolder
[]
removedItems
=
new
MediaSourceHolder
[
toIndex
-
fromIndex
];
for
(
int
i
=
removedItems
.
length
-
1
;
i
>=
0
;
i
--)
{
removedItems
[
i
]
=
mediaSourceHolders
.
remove
(
fromIndex
+
i
);
}
mediaSourceHolders
.
addAll
(
Math
.
min
(
newFromIndex
,
mediaSourceHolders
.
size
()),
Arrays
.
asList
(
removedItems
));
}
/** Data class to hold playlist media sources together with meta data needed to process them. */
/* package */
static
final
class
MediaSourceHolder
{
public
final
MaskingMediaSource
mediaSource
;
public
final
Object
uid
;
public
final
List
<
MediaSource
.
MediaPeriodId
>
activeMediaPeriodIds
;
public
int
firstWindowIndexInChild
;
public
boolean
isRemoved
;
public
MediaSourceHolder
(
MediaSource
mediaSource
,
boolean
useLazyPreparation
)
{
this
.
mediaSource
=
new
MaskingMediaSource
(
mediaSource
,
useLazyPreparation
);
this
.
activeMediaPeriodIds
=
new
ArrayList
<>();
this
.
uid
=
new
Object
();
}
public
void
reset
(
int
firstWindowIndexInChild
)
{
this
.
firstWindowIndexInChild
=
firstWindowIndexInChild
;
this
.
isRemoved
=
false
;
this
.
activeMediaPeriodIds
.
clear
();
}
}
/** Timeline exposing concatenated timelines of playlist media sources. */
/* package */
static
final
class
PlaylistTimeline
extends
AbstractConcatenatedTimeline
{
private
final
int
windowCount
;
private
final
int
periodCount
;
private
final
int
[]
firstPeriodInChildIndices
;
private
final
int
[]
firstWindowInChildIndices
;
private
final
Timeline
[]
timelines
;
private
final
Object
[]
uids
;
private
final
HashMap
<
Object
,
Integer
>
childIndexByUid
;
public
PlaylistTimeline
(
Collection
<
MediaSourceHolder
>
mediaSourceHolders
,
ShuffleOrder
shuffleOrder
)
{
super
(
/* isAtomic= */
false
,
shuffleOrder
);
int
childCount
=
mediaSourceHolders
.
size
();
firstPeriodInChildIndices
=
new
int
[
childCount
];
firstWindowInChildIndices
=
new
int
[
childCount
];
timelines
=
new
Timeline
[
childCount
];
uids
=
new
Object
[
childCount
];
childIndexByUid
=
new
HashMap
<>();
int
index
=
0
;
int
windowCount
=
0
;
int
periodCount
=
0
;
for
(
MediaSourceHolder
mediaSourceHolder
:
mediaSourceHolders
)
{
timelines
[
index
]
=
mediaSourceHolder
.
mediaSource
.
getTimeline
();
firstWindowInChildIndices
[
index
]
=
windowCount
;
firstPeriodInChildIndices
[
index
]
=
periodCount
;
windowCount
+=
timelines
[
index
].
getWindowCount
();
periodCount
+=
timelines
[
index
].
getPeriodCount
();
uids
[
index
]
=
mediaSourceHolder
.
uid
;
childIndexByUid
.
put
(
uids
[
index
],
index
++);
}
this
.
windowCount
=
windowCount
;
this
.
periodCount
=
periodCount
;
}
@Override
protected
int
getChildIndexByPeriodIndex
(
int
periodIndex
)
{
return
Util
.
binarySearchFloor
(
firstPeriodInChildIndices
,
periodIndex
+
1
,
false
,
false
);
}
@Override
protected
int
getChildIndexByWindowIndex
(
int
windowIndex
)
{
return
Util
.
binarySearchFloor
(
firstWindowInChildIndices
,
windowIndex
+
1
,
false
,
false
);
}
@Override
protected
int
getChildIndexByChildUid
(
Object
childUid
)
{
Integer
index
=
childIndexByUid
.
get
(
childUid
);
return
index
==
null
?
C
.
INDEX_UNSET
:
index
;
}
@Override
protected
Timeline
getTimelineByChildIndex
(
int
childIndex
)
{
return
timelines
[
childIndex
];
}
@Override
protected
int
getFirstPeriodIndexByChildIndex
(
int
childIndex
)
{
return
firstPeriodInChildIndices
[
childIndex
];
}
@Override
protected
int
getFirstWindowIndexByChildIndex
(
int
childIndex
)
{
return
firstWindowInChildIndices
[
childIndex
];
}
@Override
protected
Object
getChildUidByChildIndex
(
int
childIndex
)
{
return
uids
[
childIndex
];
}
@Override
public
int
getWindowCount
()
{
return
windowCount
;
}
@Override
public
int
getPeriodCount
()
{
return
periodCount
;
}
}
private
static
final
class
MediaSourceAndListener
{
public
final
MediaSource
mediaSource
;
public
final
MediaSource
.
MediaSourceCaller
caller
;
public
final
MediaSourceEventListener
eventListener
;
public
MediaSourceAndListener
(
MediaSource
mediaSource
,
MediaSource
.
MediaSourceCaller
caller
,
MediaSourceEventListener
eventListener
)
{
this
.
mediaSource
=
mediaSource
;
this
.
caller
=
caller
;
this
.
eventListener
=
eventListener
;
}
}
private
final
class
ForwardingEventListener
implements
MediaSourceEventListener
{
private
final
Playlist
.
MediaSourceHolder
id
;
private
EventDispatcher
eventDispatcher
;
public
ForwardingEventListener
(
Playlist
.
MediaSourceHolder
id
)
{
eventDispatcher
=
Playlist
.
this
.
eventDispatcher
;
this
.
id
=
id
;
}
@Override
public
void
onMediaPeriodCreated
(
int
windowIndex
,
MediaSource
.
MediaPeriodId
mediaPeriodId
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
mediaPeriodCreated
();
}
}
@Override
public
void
onMediaPeriodReleased
(
int
windowIndex
,
MediaSource
.
MediaPeriodId
mediaPeriodId
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
mediaPeriodReleased
();
}
}
@Override
public
void
onLoadStarted
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
LoadEventInfo
loadEventData
,
MediaLoadData
mediaLoadData
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
loadStarted
(
loadEventData
,
mediaLoadData
);
}
}
@Override
public
void
onLoadCompleted
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
LoadEventInfo
loadEventData
,
MediaLoadData
mediaLoadData
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
loadCompleted
(
loadEventData
,
mediaLoadData
);
}
}
@Override
public
void
onLoadCanceled
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
LoadEventInfo
loadEventData
,
MediaLoadData
mediaLoadData
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
loadCanceled
(
loadEventData
,
mediaLoadData
);
}
}
@Override
public
void
onLoadError
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
LoadEventInfo
loadEventData
,
MediaLoadData
mediaLoadData
,
IOException
error
,
boolean
wasCanceled
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
loadError
(
loadEventData
,
mediaLoadData
,
error
,
wasCanceled
);
}
}
@Override
public
void
onReadingStarted
(
int
windowIndex
,
MediaSource
.
MediaPeriodId
mediaPeriodId
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
readingStarted
();
}
}
@Override
public
void
onUpstreamDiscarded
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
MediaLoadData
mediaLoadData
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
upstreamDiscarded
(
mediaLoadData
);
}
}
@Override
public
void
onDownstreamFormatChanged
(
int
windowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
,
MediaLoadData
mediaLoadData
)
{
if
(
maybeUpdateEventDispatcher
(
windowIndex
,
mediaPeriodId
))
{
eventDispatcher
.
downstreamFormatChanged
(
mediaLoadData
);
}
}
/** Updates the event dispatcher and returns whether the event should be dispatched. */
private
boolean
maybeUpdateEventDispatcher
(
int
childWindowIndex
,
@Nullable
MediaSource
.
MediaPeriodId
childMediaPeriodId
)
{
@Nullable
MediaSource
.
MediaPeriodId
mediaPeriodId
=
null
;
if
(
childMediaPeriodId
!=
null
)
{
mediaPeriodId
=
getMediaPeriodIdForChildMediaPeriodId
(
id
,
childMediaPeriodId
);
if
(
mediaPeriodId
==
null
)
{
// Media period not found. Ignore event.
return
false
;
}
}
int
windowIndex
=
getWindowIndexForChildWindowIndex
(
id
,
childWindowIndex
);
if
(
eventDispatcher
.
windowIndex
!=
windowIndex
||
!
Util
.
areEqual
(
eventDispatcher
.
mediaPeriodId
,
mediaPeriodId
))
{
eventDispatcher
=
Playlist
.
this
.
eventDispatcher
.
withParameters
(
windowIndex
,
mediaPeriodId
,
/* mediaTimeOffsetMs= */
0L
);
}
return
true
;
}
}
}
library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
View file @
5c2806ec
...
...
@@ -19,6 +19,7 @@ import android.os.Handler;
import
android.os.Message
;
import
androidx.annotation.GuardedBy
;
import
androidx.annotation.Nullable
;
import
com.google.android.exoplayer2.AbstractConcatenatedTimeline
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Timeline
;
import
com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder
;
...
...
library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
View file @
5c2806ec
...
...
@@ -16,6 +16,7 @@
package
com
.
google
.
android
.
exoplayer2
.
source
;
import
androidx.annotation.Nullable
;
import
com.google.android.exoplayer2.AbstractConcatenatedTimeline
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.ExoPlayer
;
import
com.google.android.exoplayer2.Player
;
...
...
library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java
0 → 100644
View file @
5c2806ec
/*
* Copyright (C) 2019 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
;
import
static
com
.
google
.
common
.
truth
.
Truth
.
assertThat
;
import
static
org
.
junit
.
Assert
.
assertNotSame
;
import
static
org
.
junit
.
Assert
.
assertSame
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
import
static
org
.
mockito
.
ArgumentMatchers
.
isNull
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
static
org
.
mockito
.
Mockito
.
times
;
import
static
org
.
mockito
.
Mockito
.
verify
;
import
androidx.test.ext.junit.runners.AndroidJUnit4
;
import
com.google.android.exoplayer2.source.MediaSource
;
import
com.google.android.exoplayer2.source.ShuffleOrder
;
import
com.google.android.exoplayer2.testutil.FakeMediaSource
;
import
com.google.android.exoplayer2.testutil.FakeShuffleOrder
;
import
com.google.android.exoplayer2.testutil.FakeTimeline
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
org.junit.Before
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
/** Unit test for {@link Playlist}. */
@RunWith
(
AndroidJUnit4
.
class
)
public
class
PlaylistTest
{
private
static
final
int
PLAYLIST_SIZE
=
4
;
private
Playlist
playlist
;
@Before
public
void
setUp
()
{
playlist
=
new
Playlist
(
mock
(
Playlist
.
PlaylistInfoRefreshListener
.
class
));
}
@Test
public
void
testEmptyPlaylist_expectConstantTimelineInstanceEMPTY
()
{
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
0
);
List
<
Playlist
.
MediaSourceHolder
>
fakeHolders
=
createFakeHolders
();
Timeline
timeline
=
playlist
.
setMediaSources
(
fakeHolders
,
shuffleOrder
);
assertNotSame
(
timeline
,
Timeline
.
EMPTY
);
// Remove all media sources.
timeline
=
playlist
.
removeMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
timeline
.
getWindowCount
(),
shuffleOrder
);
assertSame
(
timeline
,
Timeline
.
EMPTY
);
timeline
=
playlist
.
setMediaSources
(
fakeHolders
,
shuffleOrder
);
assertNotSame
(
timeline
,
Timeline
.
EMPTY
);
// Clear.
timeline
=
playlist
.
clear
(
shuffleOrder
);
assertSame
(
timeline
,
Timeline
.
EMPTY
);
}
@Test
public
void
testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare
()
{
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
playlist
.
setMediaSources
(
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
),
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
2
));
// Verify prepare is called once on prepare.
verify
(
mockMediaSource1
,
times
(
0
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
0
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
assertThat
(
playlist
.
isPrepared
()).
isTrue
();
// Verify prepare is called once on prepare.
verify
(
mockMediaSource1
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
playlist
.
release
();
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
// Verify prepare is called a second time on re-prepare.
verify
(
mockMediaSource1
,
times
(
2
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
2
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
}
@Test
public
void
testSetMediaSources_playlistUnprepared_notUsingLazyPreparation
()
{
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
2
);
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
List
<
Playlist
.
MediaSourceHolder
>
mediaSources
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
);
Timeline
timeline
=
playlist
.
setMediaSources
(
mediaSources
,
shuffleOrder
);
assertThat
(
timeline
.
getWindowCount
()).
isEqualTo
(
2
);
assertThat
(
playlist
.
getSize
()).
isEqualTo
(
2
);
// Assert holder offsets have been set properly
for
(
int
i
=
0
;
i
<
mediaSources
.
size
();
i
++)
{
Playlist
.
MediaSourceHolder
mediaSourceHolder
=
mediaSources
.
get
(
i
);
assertThat
(
mediaSourceHolder
.
isRemoved
).
isFalse
();
assertThat
(
mediaSourceHolder
.
firstWindowIndexInChild
).
isEqualTo
(
i
);
}
// Set media items again. The second holder is re-used.
List
<
Playlist
.
MediaSourceHolder
>
moreMediaSources
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mock
(
MediaSource
.
class
));
moreMediaSources
.
add
(
mediaSources
.
get
(
1
));
timeline
=
playlist
.
setMediaSources
(
moreMediaSources
,
shuffleOrder
);
assertThat
(
playlist
.
getSize
()).
isEqualTo
(
2
);
assertThat
(
timeline
.
getWindowCount
()).
isEqualTo
(
2
);
for
(
int
i
=
0
;
i
<
moreMediaSources
.
size
();
i
++)
{
Playlist
.
MediaSourceHolder
mediaSourceHolder
=
moreMediaSources
.
get
(
i
);
assertThat
(
mediaSourceHolder
.
isRemoved
).
isFalse
();
assertThat
(
mediaSourceHolder
.
firstWindowIndexInChild
).
isEqualTo
(
i
);
}
// Expect removed holders and sources to be removed without releasing.
verify
(
mockMediaSource1
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSources
.
get
(
0
).
isRemoved
).
isTrue
();
// Expect re-used holder and source not to be removed.
verify
(
mockMediaSource2
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSources
.
get
(
1
).
isRemoved
).
isFalse
();
}
@Test
public
void
testSetMediaSources_playlistPrepared_notUsingLazyPreparation
()
{
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
2
);
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
List
<
Playlist
.
MediaSourceHolder
>
mediaSources
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
);
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
playlist
.
setMediaSources
(
mediaSources
,
shuffleOrder
);
// Verify sources are prepared.
verify
(
mockMediaSource1
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
// Set media items again. The second holder is re-used.
List
<
Playlist
.
MediaSourceHolder
>
moreMediaSources
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mock
(
MediaSource
.
class
));
moreMediaSources
.
add
(
mediaSources
.
get
(
1
));
playlist
.
setMediaSources
(
moreMediaSources
,
shuffleOrder
);
// Expect removed holders and sources to be removed and released.
verify
(
mockMediaSource1
,
times
(
1
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSources
.
get
(
0
).
isRemoved
).
isTrue
();
// Expect re-used holder and source not to be removed but released.
verify
(
mockMediaSource2
,
times
(
1
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSources
.
get
(
1
).
isRemoved
).
isFalse
();
verify
(
mockMediaSource2
,
times
(
2
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
}
@Test
public
void
testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared
()
{
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
List
<
Playlist
.
MediaSourceHolder
>
mediaSources
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
);
playlist
.
addMediaSources
(
/* index= */
0
,
mediaSources
,
new
ShuffleOrder
.
DefaultShuffleOrder
(
2
));
assertThat
(
playlist
.
getSize
()).
isEqualTo
(
2
);
// Verify lazy initialization does not call prepare on sources.
verify
(
mockMediaSource1
,
times
(
0
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
0
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
for
(
int
i
=
0
;
i
<
mediaSources
.
size
();
i
++)
{
assertThat
(
mediaSources
.
get
(
i
).
firstWindowIndexInChild
).
isEqualTo
(
i
);
assertThat
(
mediaSources
.
get
(
i
).
isRemoved
).
isFalse
();
}
// Add for more sources in between.
List
<
Playlist
.
MediaSourceHolder
>
moreMediaSources
=
createFakeHolders
();
playlist
.
addMediaSources
(
/* index= */
1
,
moreMediaSources
,
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
3
));
assertThat
(
mediaSources
.
get
(
0
).
firstWindowIndexInChild
).
isEqualTo
(
0
);
assertThat
(
moreMediaSources
.
get
(
0
).
firstWindowIndexInChild
).
isEqualTo
(
1
);
assertThat
(
moreMediaSources
.
get
(
3
).
firstWindowIndexInChild
).
isEqualTo
(
4
);
assertThat
(
mediaSources
.
get
(
1
).
firstWindowIndexInChild
).
isEqualTo
(
5
);
}
@Test
public
void
testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared
()
{
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
playlist
.
addMediaSources
(
/* index= */
0
,
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
),
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
2
));
// Verify prepare is called on sources when added.
verify
(
mockMediaSource1
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
verify
(
mockMediaSource2
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
}
@Test
public
void
testMoveMediaSources
()
{
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
4
);
List
<
Playlist
.
MediaSourceHolder
>
holders
=
createFakeHolders
();
playlist
.
addMediaSources
(
/* index= */
0
,
holders
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
playlist
.
moveMediaSource
(
/* currentIndex= */
0
,
/* newIndex= */
3
,
shuffleOrder
);
assertFirstWindowInChildIndices
(
holders
,
3
,
0
,
1
,
2
);
playlist
.
moveMediaSource
(
/* currentIndex= */
3
,
/* newIndex= */
0
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
2
,
/* newFromIndex= */
2
,
shuffleOrder
);
assertFirstWindowInChildIndices
(
holders
,
2
,
3
,
0
,
1
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
2
,
/* toIndex= */
4
,
/* newFromIndex= */
0
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
2
,
/* newFromIndex= */
2
,
shuffleOrder
);
assertFirstWindowInChildIndices
(
holders
,
2
,
3
,
0
,
1
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
2
,
/* toIndex= */
3
,
/* newFromIndex= */
0
,
shuffleOrder
);
assertFirstWindowInChildIndices
(
holders
,
0
,
3
,
1
,
2
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
3
,
/* toIndex= */
4
,
/* newFromIndex= */
1
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
// No-ops.
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
4
,
/* newFromIndex= */
0
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
0
,
/* newFromIndex= */
3
,
shuffleOrder
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
}
@Test
public
void
testRemoveMediaSources_whenUnprepared_expectNoRelease
()
{
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource3
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource4
=
mock
(
MediaSource
.
class
);
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
4
);
List
<
Playlist
.
MediaSourceHolder
>
holders
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
,
mockMediaSource3
,
mockMediaSource4
);
playlist
.
addMediaSources
(
/* index= */
0
,
holders
,
shuffleOrder
);
playlist
.
removeMediaSourceRange
(
/* fromIndex= */
1
,
/* toIndex= */
3
,
shuffleOrder
);
assertThat
(
playlist
.
getSize
()).
isEqualTo
(
2
);
Playlist
.
MediaSourceHolder
removedHolder1
=
holders
.
remove
(
1
);
Playlist
.
MediaSourceHolder
removedHolder2
=
holders
.
remove
(
1
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
assertThat
(
removedHolder1
.
isRemoved
).
isTrue
();
assertThat
(
removedHolder2
.
isRemoved
).
isTrue
();
verify
(
mockMediaSource1
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource2
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource3
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource4
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
}
@Test
public
void
testRemoveMediaSources_whenPrepared_expectRelease
()
{
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource3
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource4
=
mock
(
MediaSource
.
class
);
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
4
);
List
<
Playlist
.
MediaSourceHolder
>
holders
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
,
mockMediaSource3
,
mockMediaSource4
);
playlist
.
prepare
(
/* mediaTransferListener */
null
);
playlist
.
addMediaSources
(
/* index= */
0
,
holders
,
shuffleOrder
);
playlist
.
removeMediaSourceRange
(
/* fromIndex= */
1
,
/* toIndex= */
3
,
shuffleOrder
);
assertThat
(
playlist
.
getSize
()).
isEqualTo
(
2
);
holders
.
remove
(
2
);
holders
.
remove
(
1
);
assertDefaultFirstWindowInChildIndexOrder
(
holders
);
verify
(
mockMediaSource1
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource2
,
times
(
1
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource3
,
times
(
1
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
verify
(
mockMediaSource4
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
}
@Test
public
void
testRelease_playlistUnprepared_expectSourcesNotReleased
()
{
MediaSource
mockMediaSource
=
mock
(
MediaSource
.
class
);
Playlist
.
MediaSourceHolder
mediaSourceHolder
=
new
Playlist
.
MediaSourceHolder
(
mockMediaSource
,
/* useLazyPreparation= */
false
);
playlist
.
setMediaSources
(
Collections
.
singletonList
(
mediaSourceHolder
),
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
1
));
verify
(
mockMediaSource
,
times
(
0
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
playlist
.
release
();
verify
(
mockMediaSource
,
times
(
0
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSourceHolder
.
isRemoved
).
isFalse
();
}
@Test
public
void
testRelease_playlistPrepared_expectSourcesReleasedNotRemoved
()
{
MediaSource
mockMediaSource
=
mock
(
MediaSource
.
class
);
Playlist
.
MediaSourceHolder
mediaSourceHolder
=
new
Playlist
.
MediaSourceHolder
(
mockMediaSource
,
/* useLazyPreparation= */
false
);
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
playlist
.
setMediaSources
(
Collections
.
singletonList
(
mediaSourceHolder
),
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
1
));
verify
(
mockMediaSource
,
times
(
1
))
.
prepareSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
),
/* mediaTransferListener= */
isNull
());
playlist
.
release
();
verify
(
mockMediaSource
,
times
(
1
)).
releaseSource
(
any
(
MediaSource
.
MediaSourceCaller
.
class
));
assertThat
(
mediaSourceHolder
.
isRemoved
).
isFalse
();
}
@Test
public
void
testClearPlaylist_expectSourcesReleasedAndRemoved
()
{
ShuffleOrder
.
DefaultShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
4
);
MediaSource
mockMediaSource1
=
mock
(
MediaSource
.
class
);
MediaSource
mockMediaSource2
=
mock
(
MediaSource
.
class
);
List
<
Playlist
.
MediaSourceHolder
>
holders
=
createFakeHoldersWithSources
(
/* useLazyPreparation= */
false
,
mockMediaSource1
,
mockMediaSource2
);
playlist
.
setMediaSources
(
holders
,
shuffleOrder
);
playlist
.
prepare
(
/* mediaTransferListener= */
null
);
Timeline
timeline
=
playlist
.
clear
(
shuffleOrder
);
assertThat
(
timeline
.
isEmpty
()).
isTrue
();
assertThat
(
holders
.
get
(
0
).
isRemoved
).
isTrue
();
assertThat
(
holders
.
get
(
1
).
isRemoved
).
isTrue
();
verify
(
mockMediaSource1
,
times
(
1
)).
releaseSource
(
any
());
verify
(
mockMediaSource2
,
times
(
1
)).
releaseSource
(
any
());
}
@Test
public
void
testSetMediaSources_expectTimelineUsesCustomShuffleOrder
()
{
Timeline
timeline
=
playlist
.
setMediaSources
(
createFakeHolders
(),
new
FakeShuffleOrder
(
/* length=*/
4
));
assertTimelineUsesFakeShuffleOrder
(
timeline
);
}
@Test
public
void
testAddMediaSources_expectTimelineUsesCustomShuffleOrder
()
{
Timeline
timeline
=
playlist
.
addMediaSources
(
/* index= */
0
,
createFakeHolders
(),
new
FakeShuffleOrder
(
PLAYLIST_SIZE
));
assertTimelineUsesFakeShuffleOrder
(
timeline
);
}
@Test
public
void
testMoveMediaSources_expectTimelineUsesCustomShuffleOrder
()
{
ShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
PLAYLIST_SIZE
);
playlist
.
addMediaSources
(
/* index= */
0
,
createFakeHolders
(),
shuffleOrder
);
Timeline
timeline
=
playlist
.
moveMediaSource
(
/* currentIndex= */
0
,
/* newIndex= */
1
,
new
FakeShuffleOrder
(
PLAYLIST_SIZE
));
assertTimelineUsesFakeShuffleOrder
(
timeline
);
}
@Test
public
void
testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder
()
{
ShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
PLAYLIST_SIZE
);
playlist
.
addMediaSources
(
/* index= */
0
,
createFakeHolders
(),
shuffleOrder
);
Timeline
timeline
=
playlist
.
moveMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
2
,
/* newFromIndex= */
2
,
new
FakeShuffleOrder
(
PLAYLIST_SIZE
));
assertTimelineUsesFakeShuffleOrder
(
timeline
);
}
@Test
public
void
testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder
()
{
ShuffleOrder
shuffleOrder
=
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
PLAYLIST_SIZE
);
playlist
.
addMediaSources
(
/* index= */
0
,
createFakeHolders
(),
shuffleOrder
);
Timeline
timeline
=
playlist
.
removeMediaSourceRange
(
/* fromIndex= */
0
,
/* toIndex= */
2
,
new
FakeShuffleOrder
(
/* length= */
2
));
assertTimelineUsesFakeShuffleOrder
(
timeline
);
}
@Test
public
void
testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder
()
{
playlist
.
setMediaSources
(
createFakeHolders
(),
new
ShuffleOrder
.
DefaultShuffleOrder
(
/* length= */
PLAYLIST_SIZE
));
assertTimelineUsesFakeShuffleOrder
(
playlist
.
setShuffleOrder
(
new
FakeShuffleOrder
(
PLAYLIST_SIZE
)));
}
// Internal methods.
private
static
void
assertTimelineUsesFakeShuffleOrder
(
Timeline
timeline
)
{
assertThat
(
timeline
.
getNextWindowIndex
(
/* windowIndex= */
0
,
Player
.
REPEAT_MODE_OFF
,
/* shuffleModeEnabled= */
true
))
.
isEqualTo
(-
1
);
assertThat
(
timeline
.
getPreviousWindowIndex
(
/* windowIndex= */
timeline
.
getWindowCount
()
-
1
,
Player
.
REPEAT_MODE_OFF
,
/* shuffleModeEnabled= */
true
))
.
isEqualTo
(-
1
);
}
private
static
void
assertDefaultFirstWindowInChildIndexOrder
(
List
<
Playlist
.
MediaSourceHolder
>
holders
)
{
int
[]
indices
=
new
int
[
holders
.
size
()];
for
(
int
i
=
0
;
i
<
indices
.
length
;
i
++)
{
indices
[
i
]
=
i
;
}
assertFirstWindowInChildIndices
(
holders
,
indices
);
}
private
static
void
assertFirstWindowInChildIndices
(
List
<
Playlist
.
MediaSourceHolder
>
holders
,
int
...
firstWindowInChildIndices
)
{
assertThat
(
holders
).
hasSize
(
firstWindowInChildIndices
.
length
);
for
(
int
i
=
0
;
i
<
holders
.
size
();
i
++)
{
assertThat
(
holders
.
get
(
i
).
firstWindowIndexInChild
).
isEqualTo
(
firstWindowInChildIndices
[
i
]);
}
}
private
static
List
<
Playlist
.
MediaSourceHolder
>
createFakeHolders
()
{
MediaSource
fakeMediaSource
=
new
FakeMediaSource
(
new
FakeTimeline
(
1
));
List
<
Playlist
.
MediaSourceHolder
>
holders
=
new
ArrayList
<>();
for
(
int
i
=
0
;
i
<
PLAYLIST_SIZE
;
i
++)
{
holders
.
add
(
new
Playlist
.
MediaSourceHolder
(
fakeMediaSource
,
/* useLazyPreparation= */
true
));
}
return
holders
;
}
private
static
List
<
Playlist
.
MediaSourceHolder
>
createFakeHoldersWithSources
(
boolean
useLazyPreparation
,
MediaSource
...
sources
)
{
List
<
Playlist
.
MediaSourceHolder
>
holders
=
new
ArrayList
<>();
for
(
MediaSource
mediaSource
:
sources
)
{
holders
.
add
(
new
Playlist
.
MediaSourceHolder
(
mediaSource
,
/* useLazyPreparation= */
useLazyPreparation
));
}
return
holders
;
}
}
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