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
129a64c4
authored
Apr 01, 2019
by
aquilescanta
Committed by
Oliver Woodman
Apr 01, 2019
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Use getItemIds to get the actual size of the Cast media queue
Issue:#4964 PiperOrigin-RevId: 241311763
parent
334da7de
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
192 additions
and
120 deletions
RELEASENOTES.md
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
RELEASENOTES.md
View file @
129a64c4
...
...
@@ -3,6 +3,8 @@
### dev-v2 (not yet released) ###
*
Update to Mockito 2
*
Cast extension: Work around Cast framework returning a limited-size queue
items list (
[
#4964
](
https://github.com/google/ExoPlayer/issues/4964
)
).
*
Add new
`ExoPlaybackException`
types for remote exceptions and out-of-memory
errors.
*
DASH:
...
...
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
View file @
129a64c4
...
...
@@ -574,7 +574,9 @@ public final class CastPlayer extends BasePlayer {
CastTimeline
oldTimeline
=
currentTimeline
;
MediaStatus
status
=
getMediaStatus
();
currentTimeline
=
status
!=
null
?
timelineTracker
.
getCastTimeline
(
status
)
:
CastTimeline
.
EMPTY_CAST_TIMELINE
;
status
!=
null
?
timelineTracker
.
getCastTimeline
(
remoteMediaClient
)
:
CastTimeline
.
EMPTY_CAST_TIMELINE
;
return
!
oldTimeline
.
equals
(
currentTimeline
);
}
...
...
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
View file @
129a64c4
...
...
@@ -16,23 +16,65 @@
package
com
.
google
.
android
.
exoplayer2
.
ext
.
cast
;
import
androidx.annotation.Nullable
;
import
android.util.SparseArray
;
import
android.util.SparseIntArray
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Timeline
;
import
com.google.android.gms.cast.MediaInfo
;
import
com.google.android.gms.cast.MediaQueueItem
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.Map
;
/**
* A {@link Timeline} for Cast media queues.
*/
/* package */
final
class
CastTimeline
extends
Timeline
{
/** Holds {@link Timeline} related data for a Cast media item. */
public
static
final
class
ItemData
{
/** Holds no media information. */
public
static
final
ItemData
EMPTY
=
new
ItemData
();
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public
final
long
durationUs
;
/**
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public
final
long
defaultPositionUs
;
private
ItemData
()
{
this
(
/* durationUs= */
C
.
TIME_UNSET
,
/* defaultPositionUs */
C
.
TIME_UNSET
);
}
/**
* Creates an instance.
*
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
*/
public
ItemData
(
long
durationUs
,
long
defaultPositionUs
)
{
this
.
durationUs
=
durationUs
;
this
.
defaultPositionUs
=
defaultPositionUs
;
}
/** Returns an instance with the given {@link #durationsUs}. */
public
ItemData
copyWithDurationUs
(
long
durationUs
)
{
if
(
durationUs
==
this
.
durationUs
)
{
return
this
;
}
return
new
ItemData
(
durationUs
,
defaultPositionUs
);
}
/** Returns an instance with the given {@link #defaultPositionsUs}. */
public
ItemData
copyWithDefaultPositionUs
(
long
defaultPositionUs
)
{
if
(
defaultPositionUs
==
this
.
defaultPositionUs
)
{
return
this
;
}
return
new
ItemData
(
durationUs
,
defaultPositionUs
);
}
}
/** {@link Timeline} for a cast queue that has no items. */
public
static
final
CastTimeline
EMPTY_CAST_TIMELINE
=
new
CastTimeline
(
Collections
.
emptyList
(),
Collections
.
emptyMap
());
new
CastTimeline
(
new
int
[
0
],
new
SparseArray
<>
());
private
final
SparseIntArray
idsToIndex
;
private
final
int
[]
ids
;
...
...
@@ -40,28 +82,23 @@ import java.util.Map;
private
final
long
[]
defaultPositionsUs
;
/**
* @param items A list of cast media queue items to represent.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
* Creates a Cast timeline from the given data.
*
* @param itemIds The ids of the items in the timeline.
* @param itemIdToData Maps item ids to {@link ItemData}.
*/
public
CastTimeline
(
List
<
MediaQueueItem
>
items
,
Map
<
String
,
Long
>
contentIdToDurationUsMap
)
{
int
itemCount
=
items
.
size
();
int
index
=
0
;
public
CastTimeline
(
int
[]
itemIds
,
SparseArray
<
ItemData
>
itemIdToData
)
{
int
itemCount
=
itemIds
.
length
;
idsToIndex
=
new
SparseIntArray
(
itemCount
);
ids
=
new
int
[
itemCount
]
;
ids
=
Arrays
.
copyOf
(
itemIds
,
itemCount
)
;
durationsUs
=
new
long
[
itemCount
];
defaultPositionsUs
=
new
long
[
itemCount
];
for
(
MediaQueueItem
item
:
items
)
{
int
itemId
=
item
.
getItemId
();
ids
[
index
]
=
itemId
;
idsToIndex
.
put
(
itemId
,
index
);
MediaInfo
mediaInfo
=
item
.
getMedia
();
String
contentId
=
mediaInfo
.
getContentId
();
durationsUs
[
index
]
=
contentIdToDurationUsMap
.
containsKey
(
contentId
)
?
contentIdToDurationUsMap
.
get
(
contentId
)
:
CastUtils
.
getStreamDurationUs
(
mediaInfo
);
defaultPositionsUs
[
index
]
=
(
long
)
(
item
.
getStartTime
()
*
C
.
MICROS_PER_SECOND
);
index
++;
for
(
int
i
=
0
;
i
<
ids
.
length
;
i
++)
{
int
id
=
ids
[
i
];
idsToIndex
.
put
(
id
,
i
);
ItemData
data
=
itemIdToData
.
get
(
id
,
ItemData
.
EMPTY
);
durationsUs
[
i
]
=
data
.
durationUs
;
defaultPositionsUs
[
i
]
=
data
.
defaultPositionUs
;
}
}
...
...
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
View file @
129a64c4
...
...
@@ -15,53 +15,84 @@
*/
package
com
.
google
.
android
.
exoplayer2
.
ext
.
cast
;
import
com.google.android.gms.cast.MediaInfo
;
import
android.util.SparseArray
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.gms.cast.MediaQueueItem
;
import
com.google.android.gms.cast.MediaStatus
;
import
java.util.HashMap
;
import
com.google.android.gms.cast.framework.media.RemoteMediaClient
;
import
java.util.HashSet
;
import
java.util.List
;
/**
* Creates {@link CastTimeline
}s from cast receiver app media statu
s.
* Creates {@link CastTimeline
CastTimelines} from cast receiver app status update
s.
*
* <p>This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553].
*/
/* package */
final
class
CastTimelineTracker
{
private
final
HashMap
<
String
,
Long
>
contentIdToDurationUsMap
;
private
final
HashSet
<
String
>
scratchContentIdSet
;
private
final
SparseArray
<
CastTimeline
.
ItemData
>
itemIdToData
;
public
CastTimelineTracker
()
{
contentIdToDurationUsMap
=
new
HashMap
<>();
scratchContentIdSet
=
new
HashSet
<>();
itemIdToData
=
new
SparseArray
<>();
}
/**
* Returns a {@link CastTimeline} that represent the given {@code status}.
* Returns a {@link CastTimeline} that represents the state of the given {@code
* remoteMediaClient}.
*
* @param status The Cast media status.
* @return A {@link CastTimeline} that represent the given {@code status}.
* <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
* invocations of this method.
*
* @param remoteMediaClient The Cast media client.
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/
public
CastTimeline
getCastTimeline
(
MediaStatus
status
)
{
MediaInfo
mediaInfo
=
status
.
getMediaInfo
();
List
<
MediaQueueItem
>
items
=
status
.
getQueueItems
();
removeUnusedDurationEntries
(
items
);
public
CastTimeline
getCastTimeline
(
RemoteMediaClient
remoteMediaClient
)
{
int
[]
itemIds
=
remoteMediaClient
.
getMediaQueue
().
getItemIds
();
if
(
itemIds
.
length
>
0
)
{
// Only remove unused items when there is something in the queue to avoid removing all entries
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
removeUnusedItemDataEntries
(
itemIds
);
}
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
MediaStatus
mediaStatus
=
remoteMediaClient
.
getMediaStatus
();
if
(
mediaStatus
==
null
)
{
return
CastTimeline
.
EMPTY_CAST_TIMELINE
;
}
if
(
mediaInfo
!=
null
)
{
String
contentId
=
mediaInfo
.
getContentId
();
long
durationUs
=
CastUtils
.
getStreamDurationUs
(
mediaInfo
);
contentIdToDurationUsMap
.
put
(
contentId
,
durationUs
);
int
currentItemId
=
mediaStatus
.
getCurrentItemId
();
long
durationUs
=
CastUtils
.
getStreamDurationUs
(
mediaStatus
.
getMediaInfo
());
itemIdToData
.
put
(
currentItemId
,
itemIdToData
.
get
(
currentItemId
,
CastTimeline
.
ItemData
.
EMPTY
)
.
copyWithDurationUs
(
durationUs
));
for
(
MediaQueueItem
item
:
mediaStatus
.
getQueueItems
())
{
int
itemId
=
item
.
getItemId
();
itemIdToData
.
put
(
itemId
,
itemIdToData
.
get
(
itemId
,
CastTimeline
.
ItemData
.
EMPTY
)
.
copyWithDefaultPositionUs
((
long
)
(
item
.
getStartTime
()
*
C
.
MICROS_PER_SECOND
)));
}
return
new
CastTimeline
(
items
,
contentIdToDurationUsMap
);
return
new
CastTimeline
(
itemIds
,
itemIdToData
);
}
private
void
removeUnusedDurationEntries
(
List
<
MediaQueueItem
>
items
)
{
scratchContentIdSet
.
clear
();
for
(
MediaQueueItem
item
:
items
)
{
scratchContentIdSet
.
add
(
item
.
getMedia
().
getContentId
());
private
void
removeUnusedItemDataEntries
(
int
[]
itemIds
)
{
HashSet
<
Integer
>
scratchItemIds
=
new
HashSet
<>(
/* initialCapacity= */
itemIds
.
length
*
2
);
for
(
int
id
:
itemIds
)
{
scratchItemIds
.
add
(
id
);
}
int
index
=
0
;
while
(
index
<
itemIdToData
.
size
())
{
if
(!
scratchItemIds
.
contains
(
itemIdToData
.
keyAt
(
index
)))
{
itemIdToData
.
removeAt
(
index
);
}
else
{
index
++;
}
}
contentIdToDurationUsMap
.
keySet
().
retainAll
(
scratchContentIdSet
);
}
}
extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
View file @
129a64c4
...
...
@@ -31,11 +31,13 @@ import com.google.android.gms.cast.MediaTrack;
* unknown or not applicable.
*
* @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds.
* @return The duration in microseconds
, or {@link C#TIME_UNSET} if unknown or not applicable
.
*/
public
static
long
getStreamDurationUs
(
MediaInfo
mediaInfo
)
{
long
durationMs
=
mediaInfo
!=
null
?
mediaInfo
.
getStreamDuration
()
:
MediaInfo
.
UNKNOWN_DURATION
;
if
(
mediaInfo
==
null
)
{
return
C
.
TIME_UNSET
;
}
long
durationMs
=
mediaInfo
.
getStreamDuration
();
return
durationMs
!=
MediaInfo
.
UNKNOWN_DURATION
?
C
.
msToUs
(
durationMs
)
:
C
.
TIME_UNSET
;
}
...
...
extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
View file @
129a64c4
...
...
@@ -20,9 +20,10 @@ import com.google.android.exoplayer2.C;
import
com.google.android.exoplayer2.testutil.TimelineAsserts
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
com.google.android.gms.cast.MediaInfo
;
import
com.google.android.gms.cast.MediaQueueItem
;
import
com.google.android.gms.cast.MediaStatus
;
import
java.util.ArrayList
;
import
com.google.android.gms.cast.framework.media.MediaQueue
;
import
com.google.android.gms.cast.framework.media.RemoteMediaClient
;
import
java.util.Collections
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
import
org.mockito.Mockito
;
...
...
@@ -31,7 +32,6 @@ import org.mockito.Mockito;
@RunWith
(
AndroidJUnit4
.
class
)
public
class
CastTimelineTrackerTest
{
private
static
final
long
DURATION_1_MS
=
1000
;
private
static
final
long
DURATION_2_MS
=
2000
;
private
static
final
long
DURATION_3_MS
=
3000
;
private
static
final
long
DURATION_4_MS
=
4000
;
...
...
@@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
/** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test
public
void
testGetCastTimeline
()
{
MediaInfo
mediaInfo
;
MediaStatus
status
=
mockMediaStatus
(
new
int
[]
{
1
,
2
,
3
},
new
String
[]
{
"contentId1"
,
"contentId2"
,
"contentId3"
},
new
long
[]
{
DURATION_1_MS
,
MediaInfo
.
UNKNOWN_DURATION
,
MediaInfo
.
UNKNOWN_DURATION
});
public
void
testGetCastTimelinePersistsDuration
()
{
CastTimelineTracker
tracker
=
new
CastTimelineTracker
();
mediaInfo
=
getMediaInfo
(
"contentId1"
,
DURATION_1_MS
);
Mockito
.
when
(
status
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
status
),
C
.
msToUs
(
DURATION_1_MS
),
C
.
TIME_UNSET
,
C
.
TIME_UNSET
);
mediaInfo
=
getMediaInfo
(
"contentId3"
,
DURATION_3_MS
);
Mockito
.
when
(
status
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
RemoteMediaClient
remoteMediaClient
=
mockRemoteMediaClient
(
/* itemIds= */
new
int
[]
{
1
,
2
,
3
,
4
,
5
},
/* currentItemId= */
2
,
/* currentDurationMs= */
DURATION_2_MS
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
status
),
C
.
msToUs
(
DURATION_1_MS
),
tracker
.
getCastTimeline
(
remoteMediaClient
),
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_3_MS
));
C
.
msToUs
(
DURATION_2_MS
),
C
.
TIME_UNSET
,
C
.
TIME_UNSET
,
C
.
TIME_UNSET
);
mediaInfo
=
getMediaInfo
(
"contentId2"
,
DURATION_2_MS
);
Mockito
.
when
(
status
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
remoteMediaClient
=
mockRemoteMediaClient
(
/* itemIds= */
new
int
[]
{
1
,
2
,
3
},
/* currentItemId= */
3
,
/* currentDurationMs= */
DURATION_3_MS
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
status
),
C
.
msToUs
(
DURATION_1_MS
)
,
tracker
.
getCastTimeline
(
remoteMediaClient
),
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_2_MS
),
C
.
msToUs
(
DURATION_3_MS
));
MediaStatus
newStatus
=
mockMediaStatus
(
new
int
[]
{
4
,
1
,
5
,
3
},
new
String
[]
{
"contentId4"
,
"contentId1"
,
"contentId5"
,
"contentId3"
},
new
long
[]
{
MediaInfo
.
UNKNOWN_DURATION
,
MediaInfo
.
UNKNOWN_DURATION
,
DURATION_5_MS
,
MediaInfo
.
UNKNOWN_DURATION
});
mediaInfo
=
getMediaInfo
(
"contentId5"
,
DURATION_5_MS
);
Mockito
.
when
(
newStatus
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
remoteMediaClient
=
mockRemoteMediaClient
(
/* itemIds= */
new
int
[]
{
1
,
3
},
/* currentItemId= */
3
,
/* currentDurationMs= */
DURATION_3_MS
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
newStatus
),
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_1_MS
),
C
.
msToUs
(
DURATION_5_MS
),
C
.
msToUs
(
DURATION_3_MS
));
tracker
.
getCastTimeline
(
remoteMediaClient
),
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_3_MS
));
mediaInfo
=
getMediaInfo
(
"contentId3"
,
DURATION_3_MS
);
Mockito
.
when
(
newStatus
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
remoteMediaClient
=
mockRemoteMediaClient
(
/* itemIds= */
new
int
[]
{
1
,
2
,
3
,
4
,
5
},
/* currentItemId= */
4
,
/* currentDurationMs= */
DURATION_4_MS
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
newStatus
),
tracker
.
getCastTimeline
(
remoteMediaClient
),
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_1_MS
),
C
.
msToUs
(
DURATION_5_MS
),
C
.
msToUs
(
DURATION_3_MS
));
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_3_MS
),
C
.
msToUs
(
DURATION_4_MS
),
C
.
TIME_UNSET
);
mediaInfo
=
getMediaInfo
(
"contentId4"
,
DURATION_4_MS
);
Mockito
.
when
(
newStatus
.
getMediaInfo
()).
thenReturn
(
mediaInfo
);
remoteMediaClient
=
mockRemoteMediaClient
(
/* itemIds= */
new
int
[]
{
1
,
2
,
3
,
4
,
5
},
/* currentItemId= */
5
,
/* currentDurationMs= */
DURATION_5_MS
);
TimelineAsserts
.
assertPeriodDurations
(
tracker
.
getCastTimeline
(
newStatus
),
tracker
.
getCastTimeline
(
remoteMediaClient
),
C
.
TIME_UNSET
,
C
.
TIME_UNSET
,
C
.
msToUs
(
DURATION_3_MS
),
C
.
msToUs
(
DURATION_4_MS
),
C
.
msToUs
(
DURATION_1_MS
),
C
.
msToUs
(
DURATION_5_MS
),
C
.
msToUs
(
DURATION_3_MS
));
C
.
msToUs
(
DURATION_5_MS
));
}
private
static
MediaStatus
mockMediaStatus
(
int
[]
itemIds
,
String
[]
contentIds
,
long
[]
durationsMs
)
{
ArrayList
<
MediaQueueItem
>
items
=
new
ArrayList
<>();
for
(
int
i
=
0
;
i
<
contentIds
.
length
;
i
++)
{
MediaInfo
mediaInfo
=
getMediaInfo
(
contentIds
[
i
],
durationsMs
[
i
]);
MediaQueueItem
item
=
Mockito
.
mock
(
MediaQueueItem
.
class
);
Mockito
.
when
(
item
.
getMedia
()).
thenReturn
(
mediaInfo
);
Mockito
.
when
(
item
.
getItemId
()).
thenReturn
(
itemIds
[
i
]);
items
.
add
(
item
);
}
private
static
RemoteMediaClient
mockRemoteMediaClient
(
int
[]
itemIds
,
int
currentItemId
,
long
currentDurationMs
)
{
RemoteMediaClient
remoteMediaClient
=
Mockito
.
mock
(
RemoteMediaClient
.
class
);
MediaStatus
status
=
Mockito
.
mock
(
MediaStatus
.
class
);
Mockito
.
when
(
status
.
getQueueItems
()).
thenReturn
(
items
);
return
status
;
Mockito
.
when
(
status
.
getQueueItems
()).
thenReturn
(
Collections
.
emptyList
());
Mockito
.
when
(
remoteMediaClient
.
getMediaStatus
()).
thenReturn
(
status
);
Mockito
.
when
(
status
.
getMediaInfo
()).
thenReturn
(
getMediaInfo
(
currentDurationMs
));
Mockito
.
when
(
status
.
getCurrentItemId
()).
thenReturn
(
currentItemId
);
MediaQueue
mediaQueue
=
mockMediaQueue
(
itemIds
);
Mockito
.
when
(
remoteMediaClient
.
getMediaQueue
()).
thenReturn
(
mediaQueue
);
return
remoteMediaClient
;
}
private
static
MediaQueue
mockMediaQueue
(
int
[]
itemIds
)
{
MediaQueue
mediaQueue
=
Mockito
.
mock
(
MediaQueue
.
class
);
Mockito
.
when
(
mediaQueue
.
getItemIds
()).
thenReturn
(
itemIds
);
return
mediaQueue
;
}
private
static
MediaInfo
getMediaInfo
(
String
contentId
,
long
durationMs
)
{
return
new
MediaInfo
.
Builder
(
contentId
)
private
static
MediaInfo
getMediaInfo
(
long
durationMs
)
{
return
new
MediaInfo
.
Builder
(
/*contentId= */
""
)
.
setStreamDuration
(
durationMs
)
.
setContentType
(
MimeTypes
.
APPLICATION_MP4
)
.
setStreamType
(
MediaInfo
.
STREAM_TYPE_NONE
)
...
...
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