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
e3c725aa
authored
Nov 12, 2020
by
bachinger
Committed by
Christos Tsilopoulos
Nov 13, 2020
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Create chunks from parts in HlsChunkSource
Issue: #5011 PiperOrigin-RevId: 342022947
parent
2693a107
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
535 additions
and
92 deletions
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPlaylistSegmentIteratorTest.java
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java
testdata/src/test/assets/media/m3u8/live_low_latency_segments_and_parts
testdata/src/test/assets/media/m3u8/live_low_latency_segments_only
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
View file @
e3c725aa
...
@@ -19,7 +19,9 @@ import static java.lang.Math.max;
...
@@ -19,7 +19,9 @@ import static java.lang.Math.max;
import
android.net.Uri
;
import
android.net.Uri
;
import
android.os.SystemClock
;
import
android.os.SystemClock
;
import
android.util.Pair
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.VisibleForTesting
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.source.BehindLiveWindowException
;
import
com.google.android.exoplayer2.source.BehindLiveWindowException
;
...
@@ -41,10 +43,13 @@ import com.google.android.exoplayer2.util.Assertions;
...
@@ -41,10 +43,13 @@ import com.google.android.exoplayer2.util.Assertions;
import
com.google.android.exoplayer2.util.TimestampAdjuster
;
import
com.google.android.exoplayer2.util.TimestampAdjuster
;
import
com.google.android.exoplayer2.util.UriUtil
;
import
com.google.android.exoplayer2.util.UriUtil
;
import
com.google.android.exoplayer2.util.Util
;
import
com.google.android.exoplayer2.util.Util
;
import
com.google.common.collect.ImmutableList
;
import
com.google.common.collect.Iterables
;
import
com.google.common.primitives.Ints
;
import
com.google.common.primitives.Ints
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.List
;
import
org.checkerframework.checker.nullness.qual.MonotonicNonNull
;
import
org.checkerframework.checker.nullness.qual.MonotonicNonNull
;
...
@@ -242,7 +247,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -242,7 +247,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
List
<
HlsMediaChunk
>
queue
,
List
<
HlsMediaChunk
>
queue
,
boolean
allowEndOfStream
,
boolean
allowEndOfStream
,
HlsChunkHolder
out
)
{
HlsChunkHolder
out
)
{
HlsMediaChunk
previous
=
queue
.
isEmpty
()
?
null
:
queue
.
get
(
queue
.
size
()
-
1
);
@Nullable
HlsMediaChunk
previous
=
queue
.
isEmpty
()
?
null
:
Iterables
.
getLast
(
queue
);
int
oldTrackIndex
=
previous
==
null
?
C
.
INDEX_UNSET
:
trackGroup
.
indexOf
(
previous
.
trackFormat
);
int
oldTrackIndex
=
previous
==
null
?
C
.
INDEX_UNSET
:
trackGroup
.
indexOf
(
previous
.
trackFormat
);
long
bufferedDurationUs
=
loadPositionUs
-
playbackPositionUs
;
long
bufferedDurationUs
=
loadPositionUs
-
playbackPositionUs
;
long
timeToLiveEdgeUs
=
resolveTimeToLiveEdgeUs
(
playbackPositionUs
);
long
timeToLiveEdgeUs
=
resolveTimeToLiveEdgeUs
(
playbackPositionUs
);
...
@@ -275,6 +280,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -275,6 +280,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Retry when playlist is refreshed.
// Retry when playlist is refreshed.
return
;
return
;
}
}
@Nullable
HlsMediaPlaylist
mediaPlaylist
=
HlsMediaPlaylist
mediaPlaylist
=
playlistTracker
.
getPlaylistSnapshot
(
selectedPlaylistUrl
,
/* isForPlayback= */
true
);
playlistTracker
.
getPlaylistSnapshot
(
selectedPlaylistUrl
,
/* isForPlayback= */
true
);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null.
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null.
...
@@ -286,22 +292,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -286,22 +292,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Select the chunk.
// Select the chunk.
long
startOfPlaylistInPeriodUs
=
long
startOfPlaylistInPeriodUs
=
mediaPlaylist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
mediaPlaylist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
long
chunkMediaSequence
=
Pair
<
Long
,
Integer
>
nextMediaSequenceAndPartIndex
=
get
ChunkMediaSequence
(
get
NextMediaSequenceAndPartIndex
(
previous
,
switchingTrack
,
mediaPlaylist
,
startOfPlaylistInPeriodUs
,
loadPositionUs
);
previous
,
switchingTrack
,
mediaPlaylist
,
startOfPlaylistInPeriodUs
,
loadPositionUs
);
long
chunkMediaSequence
=
nextMediaSequenceAndPartIndex
.
first
;
int
partIndex
=
nextMediaSequenceAndPartIndex
.
second
;
if
(
chunkMediaSequence
<
mediaPlaylist
.
mediaSequence
&&
previous
!=
null
&&
switchingTrack
)
{
if
(
chunkMediaSequence
<
mediaPlaylist
.
mediaSequence
&&
previous
!=
null
&&
switchingTrack
)
{
// We try getting the next chunk without adapting in case that's the reason for falling
// We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window.
// behind the live window.
selectedTrackIndex
=
oldTrackIndex
;
selectedTrackIndex
=
oldTrackIndex
;
selectedPlaylistUrl
=
playlistUrls
[
selectedTrackIndex
];
selectedPlaylistUrl
=
playlistUrls
[
selectedTrackIndex
];
mediaPlaylist
=
mediaPlaylist
=
playlistTracker
.
getPlaylistSnapshot
(
selectedPlaylistUrl
,
/* isForPlayback= */
true
);
playlistTracker
.
getPlaylistSnapshot
(
selectedPlaylistUrl
,
/* isForPlayback= */
true
);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be
// non-null.
// non-null.
Assertions
.
checkNotNull
(
mediaPlaylist
);
Assertions
.
checkNotNull
(
mediaPlaylist
);
startOfPlaylistInPeriodUs
=
startOfPlaylistInPeriodUs
=
mediaPlaylist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
mediaPlaylist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
chunkMediaSequence
=
previous
.
getNextChunkIndex
();
// Get the next segment/part without switching tracks.
Pair
<
Long
,
Integer
>
nextMediaSequenceAndPartIndexWithoutAdapting
=
getNextMediaSequenceAndPartIndex
(
previous
,
/* switchingTrack= */
false
,
mediaPlaylist
,
startOfPlaylistInPeriodUs
,
loadPositionUs
);
chunkMediaSequence
=
nextMediaSequenceAndPartIndexWithoutAdapting
.
first
;
partIndex
=
nextMediaSequenceAndPartIndexWithoutAdapting
.
second
;
}
}
if
(
chunkMediaSequence
<
mediaPlaylist
.
mediaSequence
)
{
if
(
chunkMediaSequence
<
mediaPlaylist
.
mediaSequence
)
{
...
@@ -309,36 +326,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -309,36 +326,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return
;
return
;
}
}
int
segmentIndexInPlaylist
=
(
int
)
(
chunkMediaSequence
-
mediaPlaylist
.
mediaSequence
);
@Nullable
int
availableSegmentCount
=
mediaPlaylist
.
segments
.
size
();
SegmentBaseHolder
segmentBaseHolder
=
if
(
segmentIndexInPlaylist
>=
availableSegmentCount
)
{
getNextSegmentHolder
(
mediaPlaylist
,
chunkMediaSequence
,
partIndex
);
if
(
mediaPlaylist
.
hasEndTag
)
{
if
(
segmentBaseHolder
==
null
)
{
if
(
allowEndOfStream
||
availableSegmentCount
==
0
)
{
if
(!
mediaPlaylist
.
hasEndTag
)
{
out
.
endOfStream
=
true
;
// Reload the playlist in case of a live stream.
return
;
}
segmentIndexInPlaylist
=
availableSegmentCount
-
1
;
}
else
/* Live */
{
out
.
playlistUrl
=
selectedPlaylistUrl
;
out
.
playlistUrl
=
selectedPlaylistUrl
;
seenExpectedPlaylistError
&=
selectedPlaylistUrl
.
equals
(
expectedPlaylistUrl
);
seenExpectedPlaylistError
&=
selectedPlaylistUrl
.
equals
(
expectedPlaylistUrl
);
expectedPlaylistUrl
=
selectedPlaylistUrl
;
expectedPlaylistUrl
=
selectedPlaylistUrl
;
return
;
return
;
}
else
if
(
allowEndOfStream
||
mediaPlaylist
.
segments
.
isEmpty
())
{
out
.
endOfStream
=
true
;
return
;
}
}
// Use the last segment available in case of a VOD stream.
segmentBaseHolder
=
new
SegmentBaseHolder
(
Iterables
.
getLast
(
mediaPlaylist
.
segments
),
mediaPlaylist
.
mediaSequence
+
mediaPlaylist
.
segments
.
size
()
-
1
,
/* partIndex= */
C
.
INDEX_UNSET
);
}
}
// We have a valid playlist snapshot, we can discard any playlist errors at this point.
// We have a valid media segment, we can discard any playlist errors at this point.
seenExpectedPlaylistError
=
false
;
seenExpectedPlaylistError
=
false
;
expectedPlaylistUrl
=
null
;
expectedPlaylistUrl
=
null
;
// Handle encryption.
// Check if the media segment or its initialization segment are fully encrypted.
HlsMediaPlaylist
.
Segment
segment
=
mediaPlaylist
.
segments
.
get
(
segmentIndexInPlaylist
);
@Nullable
Uri
initSegmentKeyUri
=
// Check if the segment or its initialization segment are fully encrypted.
getFullEncryptionKeyUri
(
mediaPlaylist
,
segmentBaseHolder
.
segmentBase
.
initializationSegment
);
Uri
initSegmentKeyUri
=
getFullEncryptionKeyUri
(
mediaPlaylist
,
segment
.
initializationSegment
);
out
.
chunk
=
maybeCreateEncryptionChunkFor
(
initSegmentKeyUri
,
selectedTrackIndex
);
out
.
chunk
=
maybeCreateEncryptionChunkFor
(
initSegmentKeyUri
,
selectedTrackIndex
);
if
(
out
.
chunk
!=
null
)
{
if
(
out
.
chunk
!=
null
)
{
return
;
return
;
}
}
Uri
mediaSegmentKeyUri
=
getFullEncryptionKeyUri
(
mediaPlaylist
,
segment
);
@Nullable
Uri
mediaSegmentKeyUri
=
getFullEncryptionKeyUri
(
mediaPlaylist
,
segmentBaseHolder
.
segmentBase
);
out
.
chunk
=
maybeCreateEncryptionChunkFor
(
mediaSegmentKeyUri
,
selectedTrackIndex
);
out
.
chunk
=
maybeCreateEncryptionChunkFor
(
mediaSegmentKeyUri
,
selectedTrackIndex
);
if
(
out
.
chunk
!=
null
)
{
if
(
out
.
chunk
!=
null
)
{
return
;
return
;
...
@@ -351,7 +374,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -351,7 +374,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
playlistFormats
[
selectedTrackIndex
],
playlistFormats
[
selectedTrackIndex
],
startOfPlaylistInPeriodUs
,
startOfPlaylistInPeriodUs
,
mediaPlaylist
,
mediaPlaylist
,
segment
IndexInPlaylist
,
segment
BaseHolder
,
selectedPlaylistUrl
,
selectedPlaylistUrl
,
muxedCaptionFormats
,
muxedCaptionFormats
,
trackSelection
.
getSelectionReason
(),
trackSelection
.
getSelectionReason
(),
...
@@ -363,6 +386,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -363,6 +386,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* initSegmentKey= */
keyCache
.
get
(
initSegmentKeyUri
));
/* initSegmentKey= */
keyCache
.
get
(
initSegmentKeyUri
));
}
}
@Nullable
private
static
SegmentBaseHolder
getNextSegmentHolder
(
HlsMediaPlaylist
mediaPlaylist
,
long
nextMediaSequence
,
int
nextPartIndex
)
{
int
segmentIndexInPlaylist
=
(
int
)
(
nextMediaSequence
-
mediaPlaylist
.
mediaSequence
);
if
(
segmentIndexInPlaylist
==
mediaPlaylist
.
segments
.
size
())
{
int
index
=
nextPartIndex
!=
C
.
INDEX_UNSET
?
nextPartIndex
:
0
;
return
index
<
mediaPlaylist
.
trailingParts
.
size
()
?
new
SegmentBaseHolder
(
mediaPlaylist
.
trailingParts
.
get
(
index
),
nextMediaSequence
,
index
)
:
null
;
}
Segment
mediaSegment
=
mediaPlaylist
.
segments
.
get
(
segmentIndexInPlaylist
);
if
(
nextPartIndex
==
C
.
INDEX_UNSET
)
{
return
new
SegmentBaseHolder
(
mediaSegment
,
nextMediaSequence
,
nextPartIndex
);
}
if
(
nextPartIndex
<
mediaSegment
.
parts
.
size
())
{
// The requested part is available in the requested segment.
return
new
SegmentBaseHolder
(
mediaSegment
.
parts
.
get
(
nextPartIndex
),
nextMediaSequence
,
nextPartIndex
);
}
else
if
(
segmentIndexInPlaylist
+
1
<
mediaPlaylist
.
segments
.
size
())
{
// The first part of the next segment is requested, but we can use the next full segment.
return
new
SegmentBaseHolder
(
mediaPlaylist
.
segments
.
get
(
segmentIndexInPlaylist
+
1
),
nextMediaSequence
+
1
,
/* partIndex= */
C
.
INDEX_UNSET
);
}
else
if
(!
mediaPlaylist
.
trailingParts
.
isEmpty
())
{
// The part index is rolling over to the first trailing part.
return
new
SegmentBaseHolder
(
mediaPlaylist
.
trailingParts
.
get
(
0
),
nextMediaSequence
+
1
,
/* partIndex= */
0
);
}
return
null
;
}
/**
/**
* Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
* Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
* source.
* source.
...
@@ -438,6 +495,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -438,6 +495,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
chunkIterators
[
i
]
=
MediaChunkIterator
.
EMPTY
;
chunkIterators
[
i
]
=
MediaChunkIterator
.
EMPTY
;
continue
;
continue
;
}
}
@Nullable
HlsMediaPlaylist
playlist
=
HlsMediaPlaylist
playlist
=
playlistTracker
.
getPlaylistSnapshot
(
playlistUrl
,
/* isForPlayback= */
false
);
playlistTracker
.
getPlaylistSnapshot
(
playlistUrl
,
/* isForPlayback= */
false
);
// Playlist snapshot is valid (checked by if() above) so playlist must be non-null.
// Playlist snapshot is valid (checked by if() above) so playlist must be non-null.
...
@@ -445,16 +503,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -445,16 +503,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long
startOfPlaylistInPeriodUs
=
long
startOfPlaylistInPeriodUs
=
playlist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
playlist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
boolean
switchingTrack
=
trackIndex
!=
oldTrackIndex
;
boolean
switchingTrack
=
trackIndex
!=
oldTrackIndex
;
long
chunkMediaSequence
=
Pair
<
Long
,
Integer
>
chunkMediaSequenceAndPartIndex
=
get
ChunkMediaSequence
(
get
NextMediaSequenceAndPartIndex
(
previous
,
switchingTrack
,
playlist
,
startOfPlaylistInPeriodUs
,
loadPositionUs
);
previous
,
switchingTrack
,
playlist
,
startOfPlaylistInPeriodUs
,
loadPositionUs
);
if
(
chunkMediaSequence
<
playlist
.
mediaSequence
)
{
long
chunkMediaSequence
=
chunkMediaSequenceAndPartIndex
.
first
;
chunkIterators
[
i
]
=
MediaChunkIterator
.
EMPTY
;
int
partIndex
=
chunkMediaSequenceAndPartIndex
.
second
;
continue
;
}
int
chunkIndex
=
(
int
)
(
chunkMediaSequence
-
playlist
.
mediaSequence
);
chunkIterators
[
i
]
=
chunkIterators
[
i
]
=
new
HlsMediaPlaylistSegmentIterator
(
playlist
,
startOfPlaylistInPeriodUs
,
chunkIndex
);
new
HlsMediaPlaylistSegmentIterator
(
playlist
.
baseUri
,
startOfPlaylistInPeriodUs
,
getSegmentBaseList
(
playlist
,
chunkMediaSequence
,
partIndex
));
}
}
return
chunkIterators
;
return
chunkIterators
;
}
}
...
@@ -495,10 +553,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -495,10 +553,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return
trackSelection
.
shouldCancelChunkLoad
(
playbackPositionUs
,
loadingChunk
,
queue
);
return
trackSelection
.
shouldCancelChunkLoad
(
playbackPositionUs
,
loadingChunk
,
queue
);
}
}
// Package methods.
/**
* Returns a list with all segment bases in the playlist starting from {@code mediaSequence} and
* {@code partIndex} in the given playlist. The list may be empty if the starting point is not in
* the playlist.
*/
@VisibleForTesting
/* package */
static
List
<
HlsMediaPlaylist
.
SegmentBase
>
getSegmentBaseList
(
HlsMediaPlaylist
playlist
,
long
mediaSequence
,
int
partIndex
)
{
int
firstSegmentIndexInPlaylist
=
(
int
)
(
mediaSequence
-
playlist
.
mediaSequence
);
if
(
firstSegmentIndexInPlaylist
<
0
||
playlist
.
segments
.
size
()
<
firstSegmentIndexInPlaylist
)
{
// The first media sequence is not in the playlist.
return
ImmutableList
.
of
();
}
List
<
HlsMediaPlaylist
.
SegmentBase
>
segmentBases
=
new
ArrayList
<>();
if
(
firstSegmentIndexInPlaylist
<
playlist
.
segments
.
size
())
{
if
(
partIndex
!=
C
.
INDEX_UNSET
)
{
// The iterator starts with a part that belongs to a segment.
Segment
firstSegment
=
playlist
.
segments
.
get
(
firstSegmentIndexInPlaylist
);
if
(
partIndex
==
0
)
{
// Use the full segment instead of the first part.
segmentBases
.
add
(
firstSegment
);
}
else
if
(
partIndex
<
firstSegment
.
parts
.
size
())
{
// Add the parts from the first requested segment.
segmentBases
.
addAll
(
firstSegment
.
parts
.
subList
(
partIndex
,
firstSegment
.
parts
.
size
()));
}
firstSegmentIndexInPlaylist
++;
}
partIndex
=
0
;
// Add all remaining segments.
segmentBases
.
addAll
(
playlist
.
segments
.
subList
(
firstSegmentIndexInPlaylist
,
playlist
.
segments
.
size
()));
}
if
(
playlist
.
partTargetDurationUs
!=
C
.
TIME_UNSET
)
{
// That's a low latency playlist.
partIndex
=
partIndex
==
C
.
INDEX_UNSET
?
0
:
partIndex
;
if
(
partIndex
<
playlist
.
trailingParts
.
size
())
{
segmentBases
.
addAll
(
playlist
.
trailingParts
.
subList
(
partIndex
,
playlist
.
trailingParts
.
size
()));
}
}
return
Collections
.
unmodifiableList
(
segmentBases
);
}
// Private methods.
// Private methods.
/**
/**
* Returns the media sequence number
of the segment to load next in
{@code mediaPlaylist}.
* Returns the media sequence number
and part index to load next in the
{@code mediaPlaylist}.
*
*
* @param previous The last (at least partially) loaded segment.
* @param previous The last (at least partially) loaded segment.
* @param switchingTrack Whether the segment to load is not preceded by a segment in the same
* @param switchingTrack Whether the segment to load is not preceded by a segment in the same
...
@@ -507,9 +611,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -507,9 +611,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period
* @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period
* start in microseconds.
* start in microseconds.
* @param loadPositionUs The current load position relative to the period start in microseconds.
* @param loadPositionUs The current load position relative to the period start in microseconds.
* @return The media sequence
of the segment
to load.
* @return The media sequence
and part index
to load.
*/
*/
private
long
getChunkMediaSequence
(
private
Pair
<
Long
,
Integer
>
getNextMediaSequenceAndPartIndex
(
@Nullable
HlsMediaChunk
previous
,
@Nullable
HlsMediaChunk
previous
,
boolean
switchingTrack
,
boolean
switchingTrack
,
HlsMediaPlaylist
mediaPlaylist
,
HlsMediaPlaylist
mediaPlaylist
,
...
@@ -521,17 +625,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -521,17 +625,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(
previous
==
null
||
independentSegments
)
?
loadPositionUs
:
previous
.
startTimeUs
;
(
previous
==
null
||
independentSegments
)
?
loadPositionUs
:
previous
.
startTimeUs
;
if
(!
mediaPlaylist
.
hasEndTag
&&
targetPositionInPeriodUs
>=
endOfPlaylistInPeriodUs
)
{
if
(!
mediaPlaylist
.
hasEndTag
&&
targetPositionInPeriodUs
>=
endOfPlaylistInPeriodUs
)
{
// If the playlist is too old to contain the chunk, we need to refresh it.
// If the playlist is too old to contain the chunk, we need to refresh it.
return
mediaPlaylist
.
mediaSequence
+
mediaPlaylist
.
segments
.
size
();
return
new
Pair
<>(
mediaPlaylist
.
mediaSequence
+
mediaPlaylist
.
segments
.
size
(),
/* partIndex */
C
.
INDEX_UNSET
);
}
}
long
targetPositionInPlaylistUs
=
targetPositionInPeriodUs
-
startOfPlaylistInPeriodUs
;
long
targetPositionInPlaylistUs
=
targetPositionInPeriodUs
-
startOfPlaylistInPeriodUs
;
return
Util
.
binarySearchFloor
(
long
mediaSequence
=
mediaPlaylist
.
segments
,
Util
.
binarySearchFloor
(
/* value= */
targetPositionInPlaylistUs
,
mediaPlaylist
.
segments
,
/* inclusive= */
true
,
/* value= */
targetPositionInPlaylistUs
,
/* stayInBounds= */
!
playlistTracker
.
isLive
()
||
previous
==
null
)
/* inclusive= */
true
,
+
mediaPlaylist
.
mediaSequence
;
/* stayInBounds= */
!
playlistTracker
.
isLive
()
||
previous
==
null
)
}
+
mediaPlaylist
.
mediaSequence
;
return
previous
.
isLoadCompleted
()
?
previous
.
getNextChunkIndex
()
:
previous
.
chunkIndex
;
return
new
Pair
<>(
mediaSequence
,
/* partIndex */
C
.
INDEX_UNSET
);
}
// If loading has not completed, we return the previous chunk again.
return
(
previous
.
isLoadCompleted
()
?
new
Pair
<>(
previous
.
partIndex
==
C
.
INDEX_UNSET
?
previous
.
getNextChunkIndex
()
:
previous
.
chunkIndex
,
previous
.
partIndex
==
C
.
INDEX_UNSET
?
C
.
INDEX_UNSET
:
previous
.
partIndex
+
1
)
:
new
Pair
<>(
previous
.
chunkIndex
,
previous
.
partIndex
));
}
}
private
long
resolveTimeToLiveEdgeUs
(
long
playbackPositionUs
)
{
private
long
resolveTimeToLiveEdgeUs
(
long
playbackPositionUs
)
{
...
@@ -574,11 +689,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -574,11 +689,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
@Nullable
@Nullable
private
static
Uri
getFullEncryptionKeyUri
(
HlsMediaPlaylist
playlist
,
@Nullable
Segment
segment
)
{
private
static
Uri
getFullEncryptionKeyUri
(
if
(
segment
==
null
||
segment
.
fullSegmentEncryptionKeyUri
==
null
)
{
HlsMediaPlaylist
playlist
,
@Nullable
HlsMediaPlaylist
.
SegmentBase
segmentBase
)
{
if
(
segmentBase
==
null
||
segmentBase
.
fullSegmentEncryptionKeyUri
==
null
)
{
return
null
;
return
null
;
}
}
return
UriUtil
.
resolveToUri
(
playlist
.
baseUri
,
segment
.
fullSegmentEncryptionKeyUri
);
return
UriUtil
.
resolveToUri
(
playlist
.
baseUri
,
segmentBase
.
fullSegmentEncryptionKeyUri
);
}
// Package classes.
/* package */
static
final
class
SegmentBaseHolder
{
public
final
HlsMediaPlaylist
.
SegmentBase
segmentBase
;
public
final
long
mediaSequence
;
public
final
int
partIndex
;
/** Creates a new instance. */
public
SegmentBaseHolder
(
HlsMediaPlaylist
.
SegmentBase
segmentBase
,
long
mediaSequence
,
int
partIndex
)
{
this
.
segmentBase
=
segmentBase
;
this
.
mediaSequence
=
mediaSequence
;
this
.
partIndex
=
partIndex
;
}
}
}
// Private classes.
// Private classes.
...
@@ -665,48 +798,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -665,48 +798,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
/** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */
@VisibleForTesting
private
static
final
class
HlsMediaPlaylistSegmentIterator
extends
BaseMediaChunkIterator
{
/* package */
static
final
class
HlsMediaPlaylistSegmentIterator
extends
BaseMediaChunkIterator
{
private
final
HlsMediaPlaylist
playlist
;
private
final
List
<
HlsMediaPlaylist
.
SegmentBase
>
segmentBases
;
private
final
long
startOfPlaylistInPeriodUs
;
private
final
long
startOfPlaylistInPeriodUs
;
private
final
String
playlistBaseUri
;
/**
/**
* Creates
iterator
.
* Creates
an iterator instance wrapping a list of {@link HlsMediaPlaylist.SegmentBase}
.
*
*
* @param playlist
The {@link HlsMediaPlaylist} to wrap
.
* @param playlist
BaseUri The base URI of the {@link HlsMediaPlaylist}
.
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in
* microseconds.
* microseconds.
* @param
chunkIndex The index of the first available chunk in the playlist
.
* @param
segmentBases The list of {@link HlsMediaPlaylist.SegmentBase segment bases} to wrap
.
*/
*/
public
HlsMediaPlaylistSegmentIterator
(
public
HlsMediaPlaylistSegmentIterator
(
HlsMediaPlaylist
playlist
,
long
startOfPlaylistInPeriodUs
,
int
chunkIndex
)
{
String
playlistBaseUri
,
super
(
/* fromIndex= */
chunkIndex
,
/* toIndex= */
playlist
.
segments
.
size
()
-
1
);
long
startOfPlaylistInPeriodUs
,
this
.
playlist
=
playlist
;
List
<
HlsMediaPlaylist
.
SegmentBase
>
segmentBases
)
{
super
(
/* fromIndex= */
0
,
segmentBases
.
size
()
-
1
);
this
.
playlistBaseUri
=
playlistBaseUri
;
this
.
startOfPlaylistInPeriodUs
=
startOfPlaylistInPeriodUs
;
this
.
startOfPlaylistInPeriodUs
=
startOfPlaylistInPeriodUs
;
this
.
segmentBases
=
segmentBases
;
}
}
@Override
@Override
public
DataSpec
getDataSpec
()
{
public
DataSpec
getDataSpec
()
{
checkInBounds
();
checkInBounds
();
Segment
segment
=
playlist
.
segment
s
.
get
((
int
)
getCurrentIndex
());
HlsMediaPlaylist
.
SegmentBase
segmentBase
=
segmentBase
s
.
get
((
int
)
getCurrentIndex
());
Uri
chunkUri
=
UriUtil
.
resolveToUri
(
playlist
.
baseUri
,
segment
.
url
);
Uri
chunkUri
=
UriUtil
.
resolveToUri
(
playlist
BaseUri
,
segmentBase
.
url
);
return
new
DataSpec
(
chunkUri
,
segment
.
byteRangeOffset
,
segment
.
byteRangeLength
);
return
new
DataSpec
(
chunkUri
,
segment
Base
.
byteRangeOffset
,
segmentBase
.
byteRangeLength
);
}
}
@Override
@Override
public
long
getChunkStartTimeUs
()
{
public
long
getChunkStartTimeUs
()
{
checkInBounds
();
checkInBounds
();
Segment
segment
=
playlist
.
segments
.
get
((
int
)
getCurrentIndex
());
return
startOfPlaylistInPeriodUs
return
startOfPlaylistInPeriodUs
+
segment
.
relativeStartTimeUs
;
+
segmentBases
.
get
((
int
)
getCurrentIndex
())
.
relativeStartTimeUs
;
}
}
@Override
@Override
public
long
getChunkEndTimeUs
()
{
public
long
getChunkEndTimeUs
()
{
checkInBounds
();
checkInBounds
();
Segment
segment
=
playlist
.
segment
s
.
get
((
int
)
getCurrentIndex
());
HlsMediaPlaylist
.
SegmentBase
segmentBase
=
segmentBase
s
.
get
((
int
)
getCurrentIndex
());
long
segmentStartTimeInPeriodUs
=
startOfPlaylistInPeriodUs
+
segment
.
relativeStartTimeUs
;
long
segmentStartTimeInPeriodUs
=
startOfPlaylistInPeriodUs
+
segment
Base
.
relativeStartTimeUs
;
return
segmentStartTimeInPeriodUs
+
segment
.
durationUs
;
return
segmentStartTimeInPeriodUs
+
segment
Base
.
durationUs
;
}
}
}
}
}
}
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
View file @
e3c725aa
...
@@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param format The chunk format.
* @param format The chunk format.
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
* @param mediaPlaylist The media playlist from which this chunk was obtained.
* @param mediaPlaylist The media playlist from which this chunk was obtained.
* @param segment
IndexInPlaylist The index of the segment in the media playlist
.
* @param segment
BaseHolder The segment holder
.
* @param playlistUrl The url of the playlist from which this chunk was obtained.
* @param playlistUrl The url of the playlist from which this chunk was obtained.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist.
* information is available in the master playlist.
...
@@ -79,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -79,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Format
format
,
Format
format
,
long
startOfPlaylistInPeriodUs
,
long
startOfPlaylistInPeriodUs
,
HlsMediaPlaylist
mediaPlaylist
,
HlsMediaPlaylist
mediaPlaylist
,
int
segmentIndexInPlaylist
,
HlsChunkSource
.
SegmentBaseHolder
segmentBaseHolder
,
Uri
playlistUrl
,
Uri
playlistUrl
,
@Nullable
List
<
Format
>
muxedCaptionFormats
,
@Nullable
List
<
Format
>
muxedCaptionFormats
,
int
trackSelectionReason
,
int
trackSelectionReason
,
...
@@ -90,7 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -90,7 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable
byte
[]
mediaSegmentKey
,
@Nullable
byte
[]
mediaSegmentKey
,
@Nullable
byte
[]
initSegmentKey
)
{
@Nullable
byte
[]
initSegmentKey
)
{
// Media segment.
// Media segment.
HlsMediaPlaylist
.
Segment
mediaSegment
=
mediaPlaylist
.
segments
.
get
(
segmentIndexInPlaylist
)
;
HlsMediaPlaylist
.
Segment
Base
mediaSegment
=
segmentBaseHolder
.
segmentBase
;
DataSpec
dataSpec
=
DataSpec
dataSpec
=
new
DataSpec
(
new
DataSpec
(
UriUtil
.
resolveToUri
(
mediaPlaylist
.
baseUri
,
mediaSegment
.
url
),
UriUtil
.
resolveToUri
(
mediaPlaylist
.
baseUri
,
mediaSegment
.
url
),
...
@@ -136,10 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -136,10 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
playlistUrl
.
equals
(
previousChunk
.
playlistUrl
)
&&
previousChunk
.
loadCompleted
;
playlistUrl
.
equals
(
previousChunk
.
playlistUrl
)
&&
previousChunk
.
loadCompleted
;
id3Decoder
=
previousChunk
.
id3Decoder
;
id3Decoder
=
previousChunk
.
id3Decoder
;
scratchId3Data
=
previousChunk
.
scratchId3Data
;
scratchId3Data
=
previousChunk
.
scratchId3Data
;
boolean
isIndependent
=
isIndependent
(
segmentBaseHolder
,
mediaPlaylist
);
boolean
canContinueWithoutSplice
=
boolean
canContinueWithoutSplice
=
isFollowingChunk
isFollowingChunk
||
(
mediaPlaylist
.
hasIndependentSegments
||
(
isIndependent
&&
segmentStartTimeInPeriodUs
>=
previousChunk
.
endTimeUs
);
&&
segmentStartTimeInPeriodUs
>=
previousChunk
.
endTimeUs
);
shouldSpliceIn
=
!
canContinueWithoutSplice
;
shouldSpliceIn
=
!
canContinueWithoutSplice
;
previousExtractor
=
previousExtractor
=
isFollowingChunk
isFollowingChunk
...
@@ -152,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -152,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
scratchId3Data
=
new
ParsableByteArray
(
Id3Decoder
.
ID3_HEADER_LENGTH
);
scratchId3Data
=
new
ParsableByteArray
(
Id3Decoder
.
ID3_HEADER_LENGTH
);
shouldSpliceIn
=
false
;
shouldSpliceIn
=
false
;
}
}
return
new
HlsMediaChunk
(
return
new
HlsMediaChunk
(
extractorFactory
,
extractorFactory
,
mediaDataSource
,
mediaDataSource
,
...
@@ -168,7 +167,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -168,7 +167,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
trackSelectionData
,
trackSelectionData
,
segmentStartTimeInPeriodUs
,
segmentStartTimeInPeriodUs
,
segmentEndTimeInPeriodUs
,
segmentEndTimeInPeriodUs
,
/* chunkMediaSequence= */
mediaPlaylist
.
mediaSequence
+
segmentIndexInPlaylist
,
segmentBaseHolder
.
mediaSequence
,
segmentBaseHolder
.
partIndex
,
discontinuitySequenceNumber
,
discontinuitySequenceNumber
,
mediaSegment
.
hasGapTag
,
mediaSegment
.
hasGapTag
,
isMasterTimestampSource
,
isMasterTimestampSource
,
...
@@ -201,6 +201,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -201,6 +201,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Whether samples for this chunk should be spliced into existing samples. */
/** Whether samples for this chunk should be spliced into existing samples. */
public
final
boolean
shouldSpliceIn
;
public
final
boolean
shouldSpliceIn
;
/** The part index or {@link C#INDEX_UNSET} if the chunk is a full segment */
public
final
int
partIndex
;
@Nullable
private
final
DataSource
initDataSource
;
@Nullable
private
final
DataSource
initDataSource
;
@Nullable
private
final
DataSpec
initDataSpec
;
@Nullable
private
final
DataSpec
initDataSpec
;
@Nullable
private
final
HlsMediaChunkExtractor
previousExtractor
;
@Nullable
private
final
HlsMediaChunkExtractor
previousExtractor
;
...
@@ -243,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -243,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
long
startTimeUs
,
long
startTimeUs
,
long
endTimeUs
,
long
endTimeUs
,
long
chunkMediaSequence
,
long
chunkMediaSequence
,
int
partIndex
,
int
discontinuitySequenceNumber
,
int
discontinuitySequenceNumber
,
boolean
hasGapTag
,
boolean
hasGapTag
,
boolean
isMasterTimestampSource
,
boolean
isMasterTimestampSource
,
...
@@ -262,6 +266,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -262,6 +266,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
endTimeUs
,
endTimeUs
,
chunkMediaSequence
);
chunkMediaSequence
);
this
.
mediaSegmentEncrypted
=
mediaSegmentEncrypted
;
this
.
mediaSegmentEncrypted
=
mediaSegmentEncrypted
;
this
.
partIndex
=
partIndex
;
this
.
discontinuitySequenceNumber
=
discontinuitySequenceNumber
;
this
.
discontinuitySequenceNumber
=
discontinuitySequenceNumber
;
this
.
initDataSpec
=
initDataSpec
;
this
.
initDataSpec
=
initDataSpec
;
this
.
initDataSource
=
initDataSource
;
this
.
initDataSource
=
initDataSource
;
...
@@ -541,4 +546,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...
@@ -541,4 +546,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
}
return
dataSource
;
return
dataSource
;
}
}
private
static
boolean
isIndependent
(
HlsChunkSource
.
SegmentBaseHolder
segmentBaseHolder
,
HlsMediaPlaylist
mediaPlaylist
)
{
if
(
segmentBaseHolder
.
segmentBase
instanceof
HlsMediaPlaylist
.
Part
)
{
return
((
HlsMediaPlaylist
.
Part
)
segmentBaseHolder
.
segmentBase
).
isIndependent
||
(
segmentBaseHolder
.
partIndex
==
0
&&
mediaPlaylist
.
hasIndependentSegments
);
}
return
mediaPlaylist
.
hasIndependentSegments
;
}
}
}
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
View file @
e3c725aa
...
@@ -797,9 +797,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -797,9 +797,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
}
String
url
=
parseStringAttr
(
line
,
REGEX_URI
,
variableDefinitions
);
String
url
=
parseStringAttr
(
line
,
REGEX_URI
,
variableDefinitions
);
long
byteRangeStart
=
long
byteRangeStart
=
parseOptionalLongAttr
(
line
,
REGEX_BYTERANGE_START
,
/* defaultValue= */
0
);
parseOptionalLongAttr
(
line
,
REGEX_BYTERANGE_START
,
/* defaultValue= */
C
.
LENGTH_UNSET
);
long
byteRangeLength
=
long
byteRangeLength
=
parseOptionalLongAttr
(
line
,
REGEX_BYTERANGE_LENGTH
,
/* defaultValue= */
C
.
TIME
_UNSET
);
parseOptionalLongAttr
(
line
,
REGEX_BYTERANGE_LENGTH
,
/* defaultValue= */
C
.
LENGTH
_UNSET
);
@Nullable
@Nullable
String
segmentEncryptionIV
=
String
segmentEncryptionIV
=
getSegmentEncryptionIV
(
getSegmentEncryptionIV
(
...
@@ -811,21 +811,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
...
@@ -811,21 +811,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes
=
getPlaylistProtectionSchemes
(
encryptionScheme
,
schemeDatas
);
playlistProtectionSchemes
=
getPlaylistProtectionSchemes
(
encryptionScheme
,
schemeDatas
);
}
}
}
}
preloadPart
=
if
(
byteRangeStart
==
C
.
LENGTH_UNSET
||
byteRangeLength
!=
C
.
LENGTH_UNSET
)
{
new
Part
(
// Skip preload part if it is an unbounded range request.
url
,
preloadPart
=
initializationSegment
,
new
Part
(
/* durationUs= */
0
,
url
,
relativeDiscontinuitySequence
,
initializationSegment
,
partStartTimeUs
,
/* durationUs= */
0
,
cachedDrmInitData
,
relativeDiscontinuitySequence
,
fullSegmentEncryptionKeyUri
,
partStartTimeUs
,
segmentEncryptionIV
,
cachedDrmInitData
,
byteRangeStart
,
fullSegmentEncryptionKeyUri
,
byteRangeLength
,
segmentEncryptionIV
,
/* hasGapTag= */
false
,
byteRangeStart
!=
C
.
LENGTH_UNSET
?
byteRangeStart
:
0
,
/* isIndependent= */
false
,
byteRangeLength
,
/* isPreload= */
true
);
/* hasGapTag= */
false
,
/* isIndependent= */
false
,
/* isPreload= */
true
);
}
}
else
if
(
line
.
startsWith
(
TAG_PART
))
{
}
else
if
(
line
.
startsWith
(
TAG_PART
))
{
@Nullable
@Nullable
String
segmentEncryptionIV
=
String
segmentEncryptionIV
=
...
...
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPlaylistSegmentIteratorTest.java
0 → 100644
View file @
e3c725aa
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
source
.
hls
;
import
static
com
.
google
.
common
.
truth
.
Truth
.
assertThat
;
import
static
org
.
junit
.
Assert
.
fail
;
import
android.net.Uri
;
import
androidx.test.core.app.ApplicationProvider
;
import
androidx.test.ext.junit.runners.AndroidJUnit4
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser
;
import
com.google.android.exoplayer2.testutil.TestUtil
;
import
com.google.android.exoplayer2.upstream.DataSpec
;
import
com.google.common.collect.Iterables
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
/** Unit test for {@link HlsChunkSource.HlsMediaPlaylistSegmentIterator}. */
@RunWith
(
AndroidJUnit4
.
class
)
public
class
HlsMediaPlaylistSegmentIteratorTest
{
public
static
final
String
LOW_LATENCY_SEGMENTS_AND_PARTS
=
"media/m3u8/live_low_latency_segments_and_parts"
;
public
static
final
String
SEGMENTS_ONLY
=
"media/m3u8/live_low_latency_segments_only"
;
@Test
public
void
create_withMediaSequenceBehindLiveWindow_isEmpty
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
mediaPlaylist
.
mediaSequence
-
1
,
/* partIndex= */
C
.
INDEX_UNSET
));
assertThat
(
hlsMediaPlaylistSegmentIterator
.
next
()).
isFalse
();
}
@Test
public
void
create_withMediaSequenceBeforeTrailingPartSegment_isEmpty
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
mediaPlaylist
.
mediaSequence
+
mediaPlaylist
.
segments
.
size
()
+
1
,
/* partIndex= */
C
.
INDEX_UNSET
));
assertThat
(
hlsMediaPlaylistSegmentIterator
.
next
()).
isFalse
();
}
@Test
public
void
create_withPartIndexBeforeLastTrailingPartSegment_isEmpty
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
mediaPlaylist
.
mediaSequence
+
mediaPlaylist
.
segments
.
size
(),
/* partIndex= */
3
));
assertThat
(
hlsMediaPlaylistSegmentIterator
.
next
()).
isFalse
();
}
@Test
public
void
next_conventionalLiveStartIteratorAtSecondSegment_correctElements
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
SEGMENTS_ONLY
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
/* mediaSequence= */
11
,
/* partIndex= */
C
.
INDEX_UNSET
));
List
<
DataSpec
>
datasSpecs
=
new
ArrayList
<>();
while
(
hlsMediaPlaylistSegmentIterator
.
next
())
{
datasSpecs
.
add
(
hlsMediaPlaylistSegmentIterator
.
getDataSpec
());
}
assertThat
(
datasSpecs
).
hasSize
(
5
);
assertThat
(
datasSpecs
.
get
(
0
).
uri
.
toString
()).
isEqualTo
(
"fileSequence11.ts"
);
assertThat
(
Iterables
.
getLast
(
datasSpecs
).
uri
.
toString
()).
isEqualTo
(
"fileSequence15.ts"
);
}
@Test
public
void
next_startIteratorAtFirstSegment_correctElements
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
/* mediaSequence= */
10
,
/* partIndex= */
C
.
INDEX_UNSET
));
List
<
DataSpec
>
datasSpecs
=
new
ArrayList
<>();
while
(
hlsMediaPlaylistSegmentIterator
.
next
())
{
datasSpecs
.
add
(
hlsMediaPlaylistSegmentIterator
.
getDataSpec
());
}
assertThat
(
datasSpecs
).
hasSize
(
9
);
// The iterator starts with 6 segments.
assertThat
(
datasSpecs
.
get
(
0
).
uri
.
toString
()).
isEqualTo
(
"fileSequence10.ts"
);
// Followed by trailing parts.
assertThat
(
datasSpecs
.
get
(
6
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.0.ts"
);
// The preload part is the last.
assertThat
(
Iterables
.
getLast
(
datasSpecs
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.2.ts"
);
}
@Test
public
void
next_startIteratorAtFirstPartInaSegment_usesFullSegment
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
/* mediaSequence= */
14
,
/* partIndex= */
0
));
List
<
DataSpec
>
datasSpecs
=
new
ArrayList
<>();
while
(
hlsMediaPlaylistSegmentIterator
.
next
())
{
datasSpecs
.
add
(
hlsMediaPlaylistSegmentIterator
.
getDataSpec
());
}
assertThat
(
datasSpecs
).
hasSize
(
5
);
// The iterator starts with 6 segments.
assertThat
(
datasSpecs
.
get
(
0
).
uri
.
toString
()).
isEqualTo
(
"fileSequence14.ts"
);
assertThat
(
datasSpecs
.
get
(
1
).
uri
.
toString
()).
isEqualTo
(
"fileSequence15.ts"
);
// Followed by trailing parts.
assertThat
(
datasSpecs
.
get
(
2
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.0.ts"
);
assertThat
(
datasSpecs
.
get
(
3
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.1.ts"
);
// The preload part is the last.
assertThat
(
Iterables
.
getLast
(
datasSpecs
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.2.ts"
);
}
@Test
public
void
next_startIteratorAtTrailingPart_correctElements
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
/* mediaSequence= */
16
,
/* partIndex= */
1
));
List
<
DataSpec
>
datasSpecs
=
new
ArrayList
<>();
while
(
hlsMediaPlaylistSegmentIterator
.
next
())
{
datasSpecs
.
add
(
hlsMediaPlaylistSegmentIterator
.
getDataSpec
());
}
assertThat
(
datasSpecs
).
hasSize
(
2
);
// The iterator starts with 2 parts.
assertThat
(
datasSpecs
.
get
(
0
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.1.ts"
);
// The preload part is the last.
assertThat
(
Iterables
.
getLast
(
datasSpecs
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.2.ts"
);
}
@Test
public
void
next_startIteratorAtPartWithinSegment_correctElements
()
{
HlsMediaPlaylist
mediaPlaylist
=
getHlsMediaPlaylist
(
LOW_LATENCY_SEGMENTS_AND_PARTS
);
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
hlsMediaPlaylistSegmentIterator
=
new
HlsChunkSource
.
HlsMediaPlaylistSegmentIterator
(
mediaPlaylist
.
baseUri
,
/* startOfPlaylistInPeriodUs= */
0
,
HlsChunkSource
.
getSegmentBaseList
(
mediaPlaylist
,
/* mediaSequence= */
14
,
/* partIndex= */
1
));
List
<
DataSpec
>
datasSpecs
=
new
ArrayList
<>();
while
(
hlsMediaPlaylistSegmentIterator
.
next
())
{
datasSpecs
.
add
(
hlsMediaPlaylistSegmentIterator
.
getDataSpec
());
}
assertThat
(
datasSpecs
).
hasSize
(
7
);
// The iterator starts with 11 parts.
assertThat
(
datasSpecs
.
get
(
0
).
uri
.
toString
()).
isEqualTo
(
"fileSequence14.1.ts"
);
assertThat
(
datasSpecs
.
get
(
1
).
uri
.
toString
()).
isEqualTo
(
"fileSequence14.2.ts"
);
assertThat
(
datasSpecs
.
get
(
2
).
uri
.
toString
()).
isEqualTo
(
"fileSequence14.3.ts"
);
// Use a segment in between if possible.
assertThat
(
datasSpecs
.
get
(
3
).
uri
.
toString
()).
isEqualTo
(
"fileSequence15.ts"
);
// Then parts again.
assertThat
(
datasSpecs
.
get
(
4
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.0.ts"
);
assertThat
(
datasSpecs
.
get
(
5
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.1.ts"
);
assertThat
(
datasSpecs
.
get
(
6
).
uri
.
toString
()).
isEqualTo
(
"fileSequence16.2.ts"
);
}
private
static
HlsMediaPlaylist
getHlsMediaPlaylist
(
String
file
)
{
try
{
return
(
HlsMediaPlaylist
)
new
HlsPlaylistParser
()
.
parse
(
Uri
.
EMPTY
,
TestUtil
.
getInputStream
(
ApplicationProvider
.
getApplicationContext
(),
file
));
}
catch
(
IOException
e
)
{
fail
(
e
.
getMessage
());
}
return
null
;
}
}
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java
View file @
e3c725aa
...
@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ParserException;
...
@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ParserException;
import
com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil
;
import
com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment
;
import
com.google.android.exoplayer2.util.Util
;
import
com.google.android.exoplayer2.util.Util
;
import
com.google.common.collect.Iterables
;
import
java.io.ByteArrayInputStream
;
import
java.io.ByteArrayInputStream
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStream
;
...
@@ -362,6 +363,7 @@ public class HlsMediaPlaylistParserTest {
...
@@ -362,6 +363,7 @@ public class HlsMediaPlaylistParserTest {
assertThat
(
secondPart
.
byteRangeOffset
).
isEqualTo
(
1234
);
assertThat
(
secondPart
.
byteRangeOffset
).
isEqualTo
(
1234
);
// Assert trailing parts.
// Assert trailing parts.
HlsMediaPlaylist
.
Part
thirdPart
=
playlist
.
trailingParts
.
get
(
0
);
HlsMediaPlaylist
.
Part
thirdPart
=
playlist
.
trailingParts
.
get
(
0
);
// Assert tailing parts.
assertThat
(
thirdPart
.
byteRangeLength
).
isEqualTo
(
1000
);
assertThat
(
thirdPart
.
byteRangeLength
).
isEqualTo
(
1000
);
assertThat
(
thirdPart
.
byteRangeOffset
).
isEqualTo
(
1234
);
assertThat
(
thirdPart
.
byteRangeOffset
).
isEqualTo
(
1234
);
assertThat
(
thirdPart
.
relativeStartTimeUs
).
isEqualTo
(
8_000_000
);
assertThat
(
thirdPart
.
relativeStartTimeUs
).
isEqualTo
(
8_000_000
);
...
@@ -545,6 +547,27 @@ public class HlsMediaPlaylistParserTest {
...
@@ -545,6 +547,27 @@ public class HlsMediaPlaylistParserTest {
}
}
@Test
@Test
public
void
parseMediaPlaylist_withUnboundedPreloadHintTypePart_ignoresPreloadPart
()
throws
IOException
{
Uri
playlistUri
=
Uri
.
parse
(
"https://example.com/test.m3u8"
);
String
playlistString
=
"#EXTM3U\n"
+
"#EXT-X-TARGETDURATION:4\n"
+
"#EXT-X-VERSION:6\n"
+
"#EXT-X-MEDIA-SEQUENCE:266\n"
+
"#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n"
+
"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts,BYTERANGE-START=0\"\n"
;
InputStream
inputStream
=
new
ByteArrayInputStream
(
Util
.
getUtf8Bytes
(
playlistString
));
HlsMediaPlaylist
playlist
=
(
HlsMediaPlaylist
)
new
HlsPlaylistParser
().
parse
(
playlistUri
,
inputStream
);
assertThat
(
playlist
.
trailingParts
).
hasSize
(
1
);
assertThat
(
Iterables
.
getLast
(
playlist
.
trailingParts
).
url
).
isEqualTo
(
"part267.1.ts"
);
assertThat
(
Iterables
.
getLast
(
playlist
.
trailingParts
).
isPreload
).
isFalse
();
}
@Test
public
void
parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData
()
public
void
parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData
()
throws
IOException
{
throws
IOException
{
Uri
playlistUri
=
Uri
.
parse
(
"https://example.com/test.m3u8"
);
Uri
playlistUri
=
Uri
.
parse
(
"https://example.com/test.m3u8"
);
...
...
testdata/src/test/assets/media/m3u8/live_low_latency_segments_and_parts
0 → 100644
View file @
e3c725aa
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000400
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.3.ts"
#EXTINF:4.00000,
fileSequence14.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.3.ts"
#EXTINF:4.00000,
fileSequence15.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.1.ts"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence16.2.ts"
testdata/src/test/assets/media/m3u8/live_low_latency_segments_only
0 → 100644
View file @
e3c725aa
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXTINF:4.00000,
fileSequence13.ts
#EXTINF:4.00000,
fileSequence14.ts
#EXTINF:4.00000,
fileSequence15.ts
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