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
4a69e166
authored
Nov 26, 2021
by
kim-vde
Browse files
Options
_('Browse Files')
Download
Plain Diff
Merge pull request #9536 from TiVo:p-fix-issue-2882
PiperOrigin-RevId: 411056555
parents
f138ec98
530dd3f7
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
301 additions
and
4 deletions
RELEASENOTES.md
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java
testdata/src/test/assets/media/m3u8/media_playlist
testdata/src/test/assets/media/m3u8/media_playlist_empty
testdata/src/test/assets/media/m3u8/media_playlist_independent_segments
testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java
RELEASENOTES.md
View file @
4a69e166
...
...
@@ -11,6 +11,9 @@
*
Add a method to
`AdPlaybackState`
to allow resetting an ad group so that
it can be played again
(
[
#9615
](
https://github.com/google/ExoPlayer/issues/9615
)
).
*
HLS:
*
Support key-frame accurate seeking in HLS
(
[
#2882
](
https://github.com/google/ExoPlayer/issues/2882
)
).
### 2.16.1 (2021-11-18)
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
View file @
4a69e166
...
...
@@ -26,6 +26,7 @@ import androidx.annotation.Nullable;
import
androidx.annotation.VisibleForTesting
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.SeekParameters
;
import
com.google.android.exoplayer2.analytics.PlayerId
;
import
com.google.android.exoplayer2.source.BehindLiveWindowException
;
import
com.google.android.exoplayer2.source.TrackGroup
;
...
...
@@ -242,6 +243,53 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
/**
* Adjusts a seek position given the specified {@link SeekParameters}.
*
* @param positionUs The seek position in microseconds.
* @param seekParameters Parameters that control how the seek is performed.
* @return The adjusted seek position, in microseconds.
*/
public
long
getAdjustedSeekPositionUs
(
long
positionUs
,
SeekParameters
seekParameters
)
{
int
selectedIndex
=
trackSelection
.
getSelectedIndex
();
@Nullable
HlsMediaPlaylist
mediaPlaylist
=
selectedIndex
<
playlistUrls
.
length
&&
selectedIndex
!=
C
.
INDEX_UNSET
?
playlistTracker
.
getPlaylistSnapshot
(
playlistUrls
[
selectedIndex
],
/* isForPlayback= */
true
)
:
null
;
if
(
mediaPlaylist
==
null
||
mediaPlaylist
.
segments
.
isEmpty
()
||
!
mediaPlaylist
.
hasIndependentSegments
)
{
return
positionUs
;
}
// Segments start with sync samples (i.e., EXT-X-INDEPENDENT-SEGMENTS is set) and the playlist
// is non-empty, so we can use segment start times as sync points. Note that in the rare case
// that (a) an adaptive quality switch occurs between the adjustment and the seek being
// performed, and (b) segment start times are not aligned across variants, it's possible that
// the adjusted position may not be at a sync point when it was intended to be. However, this is
// very much an edge case, and getting it wrong is worth it for getting the vast majority of
// cases right whilst keeping the implementation relatively simple.
long
startOfPlaylistInPeriodUs
=
mediaPlaylist
.
startTimeUs
-
playlistTracker
.
getInitialStartTimeUs
();
long
relativePositionUs
=
positionUs
-
startOfPlaylistInPeriodUs
;
int
segmentIndex
=
Util
.
binarySearchFloor
(
mediaPlaylist
.
segments
,
relativePositionUs
,
/* inclusive= */
true
,
/* stayInBounds= */
true
);
long
firstSyncUs
=
mediaPlaylist
.
segments
.
get
(
segmentIndex
).
relativeStartTimeUs
;
long
secondSyncUs
=
firstSyncUs
;
if
(
segmentIndex
!=
mediaPlaylist
.
segments
.
size
()
-
1
)
{
secondSyncUs
=
mediaPlaylist
.
segments
.
get
(
segmentIndex
+
1
).
relativeStartTimeUs
;
}
return
seekParameters
.
resolveSeekPositionUs
(
relativePositionUs
,
firstSyncUs
,
secondSyncUs
)
+
startOfPlaylistInPeriodUs
;
}
/**
* Returns the publication state of the given chunk.
*
* @param mediaChunk The media chunk for which to evaluate the publication state.
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
View file @
4a69e166
...
...
@@ -421,7 +421,14 @@ public final class HlsMediaPeriod
@Override
public
long
getAdjustedSeekPositionUs
(
long
positionUs
,
SeekParameters
seekParameters
)
{
return
positionUs
;
long
seekTargetUs
=
positionUs
;
for
(
HlsSampleStreamWrapper
sampleStreamWrapper
:
enabledSampleStreamWrappers
)
{
if
(
sampleStreamWrapper
.
isVideoSampleStream
())
{
seekTargetUs
=
sampleStreamWrapper
.
getAdjustedSeekPositionUs
(
positionUs
,
seekParameters
);
break
;
}
}
return
seekTargetUs
;
}
// HlsSampleStreamWrapper.Callback implementation.
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
View file @
4a69e166
...
...
@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.C;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.FormatHolder
;
import
com.google.android.exoplayer2.ParserException
;
import
com.google.android.exoplayer2.SeekParameters
;
import
com.google.android.exoplayer2.decoder.DecoderInputBuffer
;
import
com.google.android.exoplayer2.drm.DrmInitData
;
import
com.google.android.exoplayer2.drm.DrmSession
;
...
...
@@ -584,6 +585,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
&&
exclusionDurationMs
!=
C
.
TIME_UNSET
;
}
/** Returns whether the primary sample stream is {@link C#TRACK_TYPE_VIDEO}. */
public
boolean
isVideoSampleStream
()
{
return
primarySampleQueueType
==
C
.
TRACK_TYPE_VIDEO
;
}
/**
* Adjusts a seek position given the specified {@link SeekParameters}.
*
* @param positionUs The seek position in microseconds.
* @param seekParameters Parameters that control how the seek is performed.
* @return The adjusted seek position, in microseconds.
*/
public
long
getAdjustedSeekPositionUs
(
long
positionUs
,
SeekParameters
seekParameters
)
{
return
chunkSource
.
getAdjustedSeekPositionUs
(
positionUs
,
seekParameters
);
}
// SampleStream implementation.
public
boolean
isReady
(
int
sampleQueueIndex
)
{
...
...
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java
0 → 100644
View file @
4a69e166
/*
* Copyright 2021 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
.
mockito
.
ArgumentMatchers
.
anyBoolean
;
import
static
org
.
mockito
.
ArgumentMatchers
.
eq
;
import
static
org
.
mockito
.
Mockito
.
when
;
import
android.net.Uri
;
import
androidx.test.core.app.ApplicationProvider
;
import
androidx.test.ext.junit.runners.AndroidJUnit4
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.SeekParameters
;
import
com.google.android.exoplayer2.analytics.PlayerId
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker
;
import
com.google.android.exoplayer2.testutil.ExoPlayerTestRunner
;
import
com.google.android.exoplayer2.testutil.FakeDataSource
;
import
com.google.android.exoplayer2.testutil.TestUtil
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
org.junit.Before
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
import
org.mockito.Mock
;
import
org.mockito.Mockito
;
/** Unit tests for {@link HlsChunkSource}. */
@RunWith
(
AndroidJUnit4
.
class
)
public
class
HlsChunkSourceTest
{
private
static
final
String
PLAYLIST
=
"media/m3u8/media_playlist"
;
private
static
final
String
PLAYLIST_INDEPENDENT_SEGMENTS
=
"media/m3u8/media_playlist_independent_segments"
;
private
static
final
String
PLAYLIST_EMPTY
=
"media/m3u8/media_playlist_empty"
;
private
static
final
Uri
PLAYLIST_URI
=
Uri
.
parse
(
"http://example.com/"
);
private
static
final
long
PLAYLIST_START_PERIOD_OFFSET_US
=
8_000_000L
;
private
final
HlsExtractorFactory
mockExtractorFactory
=
HlsExtractorFactory
.
DEFAULT
;
@Mock
private
HlsPlaylistTracker
mockPlaylistTracker
;
private
HlsChunkSource
testChunkSource
;
@Before
public
void
setup
()
throws
IOException
{
mockPlaylistTracker
=
Mockito
.
mock
(
HlsPlaylistTracker
.
class
);
InputStream
inputStream
=
TestUtil
.
getInputStream
(
ApplicationProvider
.
getApplicationContext
(),
PLAYLIST_INDEPENDENT_SEGMENTS
);
HlsMediaPlaylist
playlist
=
(
HlsMediaPlaylist
)
new
HlsPlaylistParser
().
parse
(
PLAYLIST_URI
,
inputStream
);
when
(
mockPlaylistTracker
.
getPlaylistSnapshot
(
eq
(
PLAYLIST_URI
),
anyBoolean
()))
.
thenReturn
(
playlist
);
testChunkSource
=
new
HlsChunkSource
(
mockExtractorFactory
,
mockPlaylistTracker
,
new
Uri
[]
{
PLAYLIST_URI
},
new
Format
[]
{
ExoPlayerTestRunner
.
VIDEO_FORMAT
},
new
DefaultHlsDataSourceFactory
(
new
FakeDataSource
.
Factory
()),
/* mediaTransferListener= */
null
,
new
TimestampAdjusterProvider
(),
/* muxedCaptionFormats= */
null
,
PlayerId
.
UNSET
);
when
(
mockPlaylistTracker
.
isSnapshotValid
(
eq
(
PLAYLIST_URI
))).
thenReturn
(
true
);
// Mock that segments totalling PLAYLIST_START_PERIOD_OFFSET_US in duration have been removed
// from the start of the playlist.
when
(
mockPlaylistTracker
.
getInitialStartTimeUs
())
.
thenReturn
(
playlist
.
startTimeUs
-
PLAYLIST_START_PERIOD_OFFSET_US
);
}
@Test
public
void
getAdjustedSeekPositionUs_previousSync
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
17_000_000
),
SeekParameters
.
PREVIOUS_SYNC
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
16_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_nextSync
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
17_000_000
),
SeekParameters
.
NEXT_SYNC
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
20_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_nextSyncAtEnd
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
24_000_000
),
SeekParameters
.
NEXT_SYNC
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
24_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_closestSyncBefore
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
17_000_000
),
SeekParameters
.
CLOSEST_SYNC
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
16_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_closestSyncAfter
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
19_000_000
),
SeekParameters
.
CLOSEST_SYNC
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
20_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_exact
()
{
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
17_000_000
),
SeekParameters
.
EXACT
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
17_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_noIndependentSegments
()
throws
IOException
{
InputStream
inputStream
=
TestUtil
.
getInputStream
(
ApplicationProvider
.
getApplicationContext
(),
PLAYLIST
);
HlsMediaPlaylist
playlist
=
(
HlsMediaPlaylist
)
new
HlsPlaylistParser
().
parse
(
PLAYLIST_URI
,
inputStream
);
when
(
mockPlaylistTracker
.
getPlaylistSnapshot
(
eq
(
PLAYLIST_URI
),
anyBoolean
()))
.
thenReturn
(
playlist
);
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
100_000_000
),
SeekParameters
.
EXACT
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
100_000_000
);
}
@Test
public
void
getAdjustedSeekPositionUs_emptyPlaylist
()
throws
IOException
{
InputStream
inputStream
=
TestUtil
.
getInputStream
(
ApplicationProvider
.
getApplicationContext
(),
PLAYLIST_EMPTY
);
HlsMediaPlaylist
playlist
=
(
HlsMediaPlaylist
)
new
HlsPlaylistParser
().
parse
(
PLAYLIST_URI
,
inputStream
);
when
(
mockPlaylistTracker
.
getPlaylistSnapshot
(
eq
(
PLAYLIST_URI
),
anyBoolean
()))
.
thenReturn
(
playlist
);
long
adjustedPositionUs
=
testChunkSource
.
getAdjustedSeekPositionUs
(
playlistTimeToPeriodTimeUs
(
100_000_000
),
SeekParameters
.
EXACT
);
assertThat
(
periodTimeToPlaylistTimeUs
(
adjustedPositionUs
)).
isEqualTo
(
100_000_000
);
}
private
static
long
playlistTimeToPeriodTimeUs
(
long
playlistTimeUs
)
{
return
playlistTimeUs
+
PLAYLIST_START_PERIOD_OFFSET_US
;
}
private
static
long
periodTimeToPlaylistTimeUs
(
long
periodTimeUs
)
{
return
periodTimeUs
-
PLAYLIST_START_PERIOD_OFFSET_US
;
}
}
testdata/src/test/assets/media/m3u8/media_playlist
0 → 100644
View file @
4a69e166
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4,
2.mp4
#EXTINF:4,
3.mp4
#EXTINF:4,
4.mp4
#EXTINF:4,
5.mp4
#EXTINF:4,
6.mp4
#EXTINF:4,
7.mp4
#EXT-X-ENDLIST
testdata/src/test/assets/media/m3u8/media_playlist_empty
0 → 100644
View file @
4a69e166
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-MAP:URI="init.mp4"
testdata/src/test/assets/media/m3u8/media_playlist_independent_segments
0 → 100644
View file @
4a69e166
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4,
2.mp4
#EXTINF:4,
3.mp4
#EXTINF:4,
4.mp4
#EXTINF:4,
5.mp4
#EXTINF:4,
6.mp4
#EXTINF:4,
7.mp4
#EXT-X-ENDLIST
testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java
View file @
4a69e166
...
...
@@ -32,7 +32,6 @@ import com.google.android.exoplayer2.util.Assertions;
import
com.google.android.exoplayer2.util.Util
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
org.checkerframework.checker.nullness.qual.MonotonicNonNull
;
/**
* A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet}
...
...
@@ -43,9 +42,13 @@ public class FakeDataSource extends BaseDataSource {
/** Factory to create a {@link FakeDataSource}. */
public
static
class
Factory
implements
DataSource
.
Factory
{
protected
@MonotonicNonNull
FakeDataSet
fakeDataSet
;
protected
FakeDataSet
fakeDataSet
;
protected
boolean
isNetwork
;
public
Factory
()
{
fakeDataSet
=
new
FakeDataSet
();
}
public
final
Factory
setFakeDataSet
(
FakeDataSet
fakeDataSet
)
{
this
.
fakeDataSet
=
fakeDataSet
;
return
this
;
...
...
@@ -58,7 +61,7 @@ public class FakeDataSource extends BaseDataSource {
@Override
public
FakeDataSource
createDataSource
()
{
return
new
FakeDataSource
(
Assertions
.
checkStateNotNull
(
fakeDataSet
)
,
isNetwork
);
return
new
FakeDataSource
(
fakeDataSet
,
isNetwork
);
}
}
...
...
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