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
8bd01a7b
authored
Jul 01, 2020
by
olly
Committed by
Oliver Woodman
Jul 03, 2020
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Discard samples on the write-side of SampleQueue
PiperOrigin-RevId: 319205008
parent
f8217140
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
264 additions
and
142 deletions
library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
View file @
8bd01a7b
...
...
@@ -480,8 +480,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
maybeNotifyDownstreamFormat
(
sampleQueueIndex
);
int
result
=
sampleQueues
[
sampleQueueIndex
].
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
,
lastSeekPositionUs
);
sampleQueues
[
sampleQueueIndex
].
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
);
if
(
result
==
C
.
RESULT_NOTHING_READ
)
{
maybeStartDeferredRetry
(
sampleQueueIndex
);
}
...
...
@@ -815,6 +814,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
loadable
.
setLoadPosition
(
Assertions
.
checkNotNull
(
seekMap
).
getSeekPoints
(
pendingResetPositionUs
).
first
.
position
,
pendingResetPositionUs
);
for
(
SampleQueue
sampleQueue
:
sampleQueues
)
{
sampleQueue
.
setStartTimeUs
(
pendingResetPositionUs
);
}
pendingResetPositionUs
=
C
.
TIME_UNSET
;
}
extractedSamplesCountAtStartOfLoad
=
getExtractedSamplesCount
();
...
...
library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
View file @
8bd01a7b
...
...
@@ -16,6 +16,7 @@
package
com
.
google
.
android
.
exoplayer2
.
source
;
import
android.os.Looper
;
import
android.util.Log
;
import
androidx.annotation.CallSuper
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.VisibleForTesting
;
...
...
@@ -52,6 +53,7 @@ public class SampleQueue implements TrackOutput {
}
@VisibleForTesting
/* package */
static
final
int
SAMPLE_CAPACITY_INCREMENT
=
1000
;
private
static
final
String
TAG
=
"SampleQueue"
;
private
final
SampleDataQueue
sampleDataQueue
;
private
final
SampleExtrasHolder
extrasHolder
;
...
...
@@ -77,6 +79,7 @@ public class SampleQueue implements TrackOutput {
private
int
relativeFirstIndex
;
private
int
readPosition
;
private
long
startTimeUs
;
private
long
largestDiscardedTimestampUs
;
private
long
largestQueuedTimestampUs
;
private
boolean
isLastSampleQueued
;
...
...
@@ -87,6 +90,8 @@ public class SampleQueue implements TrackOutput {
@Nullable
private
Format
upstreamFormat
;
@Nullable
private
Format
upstreamCommittedFormat
;
private
int
upstreamSourceId
;
private
boolean
upstreamAllSamplesAreSyncSamples
;
private
boolean
loggedUnexpectedNonSyncSample
;
private
long
sampleOffsetUs
;
private
boolean
pendingSplice
;
...
...
@@ -119,6 +124,7 @@ public class SampleQueue implements TrackOutput {
sizes
=
new
int
[
capacity
];
cryptoDatas
=
new
CryptoData
[
capacity
];
formats
=
new
Format
[
capacity
];
startTimeUs
=
Long
.
MIN_VALUE
;
largestDiscardedTimestampUs
=
Long
.
MIN_VALUE
;
largestQueuedTimestampUs
=
Long
.
MIN_VALUE
;
upstreamFormatRequired
=
true
;
...
...
@@ -155,6 +161,7 @@ public class SampleQueue implements TrackOutput {
relativeFirstIndex
=
0
;
readPosition
=
0
;
upstreamKeyframeRequired
=
true
;
startTimeUs
=
Long
.
MIN_VALUE
;
largestDiscardedTimestampUs
=
Long
.
MIN_VALUE
;
largestQueuedTimestampUs
=
Long
.
MIN_VALUE
;
isLastSampleQueued
=
false
;
...
...
@@ -167,6 +174,16 @@ public class SampleQueue implements TrackOutput {
}
/**
* Sets the start time for the queue. Samples with earlier timestamps will be discarded or have
* the {@link C#BUFFER_FLAG_DECODE_ONLY} flag set when read.
*
* @param startTimeUs The start time, in microseconds.
*/
public
final
void
setStartTimeUs
(
long
startTimeUs
)
{
this
.
startTimeUs
=
startTimeUs
;
}
/**
* Sets a source identifier for subsequent samples.
*
* @param sourceId The source identifier.
...
...
@@ -325,8 +342,6 @@ public class SampleQueue implements TrackOutput {
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
...
...
@@ -335,11 +350,9 @@ public class SampleQueue implements TrackOutput {
FormatHolder
formatHolder
,
DecoderInputBuffer
buffer
,
boolean
formatRequired
,
boolean
loadingFinished
,
long
decodeOnlyUntilUs
)
{
boolean
loadingFinished
)
{
int
result
=
readSampleMetadata
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
,
decodeOnlyUntilUs
,
extrasHolder
);
readSampleMetadata
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
,
extrasHolder
);
if
(
result
==
C
.
RESULT_BUFFER_READ
&&
!
buffer
.
isEndOfStream
()
&&
!
buffer
.
isFlagsOnly
())
{
sampleDataQueue
.
readToBuffer
(
buffer
,
extrasHolder
);
}
...
...
@@ -357,6 +370,7 @@ public class SampleQueue implements TrackOutput {
if
(
sampleIndex
<
absoluteFirstIndex
||
sampleIndex
>
absoluteFirstIndex
+
length
)
{
return
false
;
}
startTimeUs
=
Long
.
MIN_VALUE
;
readPosition
=
sampleIndex
-
absoluteFirstIndex
;
return
true
;
}
...
...
@@ -382,6 +396,7 @@ public class SampleQueue implements TrackOutput {
if
(
offset
==
-
1
)
{
return
false
;
}
startTimeUs
=
timeUs
;
readPosition
+=
offset
;
return
true
;
}
...
...
@@ -513,6 +528,22 @@ public class SampleQueue implements TrackOutput {
}
timeUs
+=
sampleOffsetUs
;
if
(
upstreamAllSamplesAreSyncSamples
)
{
if
(
timeUs
<
startTimeUs
)
{
// If we know that all samples are sync samples, we can discard those that come before the
// start time on the write side of the queue.
return
;
}
if
((
flags
&
C
.
BUFFER_FLAG_KEY_FRAME
)
==
0
)
{
// The flag should always be set unless the source content has incorrect sample metadata.
// Log a warning (once per format change, to avoid log spam) and override the flag.
if
(!
loggedUnexpectedNonSyncSample
)
{
Log
.
w
(
TAG
,
"Overriding unexpected non-sync sample for format: "
+
upstreamFormat
);
loggedUnexpectedNonSyncSample
=
true
;
}
flags
|=
C
.
BUFFER_FLAG_KEY_FRAME
;
}
}
if
(
pendingSplice
)
{
if
(!
isKeyframe
||
!
attemptSplice
(
timeUs
))
{
return
;
...
...
@@ -568,25 +599,9 @@ public class SampleQueue implements TrackOutput {
DecoderInputBuffer
buffer
,
boolean
formatRequired
,
boolean
loadingFinished
,
long
decodeOnlyUntilUs
,
SampleExtrasHolder
extrasHolder
)
{
buffer
.
waitingForKeys
=
false
;
// This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155.
// TODO: Remove it and replace it with a fix that discards samples when writing to the queue.
boolean
hasNextSample
;
int
relativeReadIndex
=
C
.
INDEX_UNSET
;
while
((
hasNextSample
=
hasNextSample
()))
{
relativeReadIndex
=
getRelativeIndex
(
readPosition
);
long
timeUs
=
timesUs
[
relativeReadIndex
];
if
(
timeUs
<
decodeOnlyUntilUs
&&
MimeTypes
.
allSamplesAreSyncSamples
(
formats
[
relativeReadIndex
].
sampleMimeType
))
{
readPosition
++;
}
else
{
break
;
}
}
if
(!
hasNextSample
)
{
if
(!
hasNextSample
())
{
if
(
loadingFinished
||
isLastSampleQueued
)
{
buffer
.
setFlags
(
C
.
BUFFER_FLAG_END_OF_STREAM
);
return
C
.
RESULT_BUFFER_READ
;
...
...
@@ -598,6 +613,7 @@ public class SampleQueue implements TrackOutput {
}
}
int
relativeReadIndex
=
getRelativeIndex
(
readPosition
);
if
(
formatRequired
||
formats
[
relativeReadIndex
]
!=
downstreamFormat
)
{
onFormatResult
(
formats
[
relativeReadIndex
],
formatHolder
);
return
C
.
RESULT_FORMAT_READ
;
...
...
@@ -610,7 +626,7 @@ public class SampleQueue implements TrackOutput {
buffer
.
setFlags
(
flags
[
relativeReadIndex
]);
buffer
.
timeUs
=
timesUs
[
relativeReadIndex
];
if
(
buffer
.
timeUs
<
decodeOnlyUntil
Us
)
{
if
(
buffer
.
timeUs
<
startTime
Us
)
{
buffer
.
addFlag
(
C
.
BUFFER_FLAG_DECODE_ONLY
);
}
if
(
buffer
.
isFlagsOnly
())
{
...
...
@@ -631,16 +647,19 @@ public class SampleQueue implements TrackOutput {
// current upstreamFormat so we can detect format changes on the read side using cheap
// referential quality.
return
false
;
}
else
if
(
Util
.
areEqual
(
format
,
upstreamCommittedFormat
))
{
}
if
(
Util
.
areEqual
(
format
,
upstreamCommittedFormat
))
{
// The format has changed back to the format of the last committed sample. If they are
// different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat
// so we can detect format changes on the read side using cheap referential equality.
upstreamFormat
=
upstreamCommittedFormat
;
return
true
;
}
else
{
upstreamFormat
=
format
;
return
true
;
}
upstreamAllSamplesAreSyncSamples
=
MimeTypes
.
allSamplesAreSyncSamples
(
upstreamFormat
.
sampleMimeType
);
loggedUnexpectedNonSyncSample
=
false
;
return
true
;
}
private
synchronized
long
discardSampleMetadataTo
(
...
...
library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
View file @
8bd01a7b
...
...
@@ -87,7 +87,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
private
long
lastSeekPositionUs
;
private
int
nextNotifyPrimaryFormatMediaChunkIndex
;
/* package */
long
decodeOnlyUntilPositionUs
;
/* package */
boolean
loadingFinished
;
/**
...
...
@@ -282,14 +281,12 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
if
(
seekToMediaChunk
!=
null
)
{
// When seeking to the start of a chunk we use the index of the first sample in the chunk
// rather than the seek position. This ensures we seek to the keyframe at the start of the
// chunk even if
the sample timestamps are slightly offset from the chunk start times
.
// chunk even if
its timestamp is slightly earlier than the advertised chunk start time
.
seekInsideBuffer
=
primarySampleQueue
.
seekTo
(
seekToMediaChunk
.
getFirstSampleIndex
(
0
));
decodeOnlyUntilPositionUs
=
0
;
}
else
{
seekInsideBuffer
=
primarySampleQueue
.
seekTo
(
positionUs
,
/* allowTimeBeyondBuffer= */
positionUs
<
getNextLoadPositionUs
());
decodeOnlyUntilPositionUs
=
lastSeekPositionUs
;
}
if
(
seekInsideBuffer
)
{
...
...
@@ -383,8 +380,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
maybeNotifyPrimaryTrackFormatChanged
();
return
primarySampleQueue
.
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
,
decodeOnlyUntilPositionUs
);
return
primarySampleQueue
.
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
);
}
@Override
...
...
@@ -577,9 +573,16 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
if
(
isMediaChunk
(
loadable
))
{
BaseMediaChunk
mediaChunk
=
(
BaseMediaChunk
)
loadable
;
if
(
pendingReset
)
{
boolean
resetToMediaChunk
=
mediaChunk
.
startTimeUs
==
pendingResetPositionUs
;
// Only enable setting of the decode only flag if we're not resetting to a chunk boundary.
decodeOnlyUntilPositionUs
=
resetToMediaChunk
?
0
:
pendingResetPositionUs
;
// Only set the queue start times if we're not seeking to a chunk boundary. If we are
// seeking to a chunk boundary then we want the queue to pass through all of the samples in
// the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk,
// even if its timestamp is slightly earlier than the advertised chunk start time.
if
(
mediaChunk
.
startTimeUs
!=
pendingResetPositionUs
)
{
primarySampleQueue
.
setStartTimeUs
(
pendingResetPositionUs
);
for
(
SampleQueue
embeddedSampleQueue
:
embeddedSampleQueues
)
{
embeddedSampleQueue
.
setStartTimeUs
(
pendingResetPositionUs
);
}
}
pendingResetPositionUs
=
C
.
TIME_UNSET
;
}
mediaChunk
.
init
(
chunkOutput
);
...
...
@@ -799,12 +802,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
return
C
.
RESULT_NOTHING_READ
;
}
maybeNotifyDownstreamFormat
();
return
sampleQueue
.
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
,
decodeOnlyUntilPositionUs
);
return
sampleQueue
.
read
(
formatHolder
,
buffer
,
formatRequired
,
loadingFinished
);
}
public
void
release
()
{
...
...
library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
View file @
8bd01a7b
...
...
@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
import
com.google.android.exoplayer2.upstream.DefaultAllocator
;
import
com.google.android.exoplayer2.util.Assertions
;
import
com.google.android.exoplayer2.util.MediaSourceEventDispatcher
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
com.google.android.exoplayer2.util.ParsableByteArray
;
import
com.google.common.primitives.Bytes
;
import
java.io.IOException
;
...
...
@@ -180,6 +181,7 @@ public final class SampleQueueTest {
assertReadSample
(
/* timeUs= */
i
*
1000
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
/* sampleData= */
new
byte
[
1
],
/* offset= */
0
,
...
...
@@ -226,9 +228,23 @@ public final class SampleQueueTest {
sampleQueue
.
sampleMetadata
(
1000
,
C
.
BUFFER_FLAG_KEY_FRAME
,
ALLOCATION_SIZE
,
0
,
null
);
assertReadFormat
(
false
,
FORMAT_1
);
assertReadSample
(
0
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
);
assertReadSample
(
0
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
);
// Assert the second sample is read without a format change.
assertReadSample
(
1000
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
);
assertReadSample
(
1000
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
);
// The same applies if the queue is empty when the formats are written.
sampleQueue
.
format
(
FORMAT_2
);
...
...
@@ -237,7 +253,14 @@ public final class SampleQueueTest {
sampleQueue
.
sampleMetadata
(
2000
,
C
.
BUFFER_FLAG_KEY_FRAME
,
ALLOCATION_SIZE
,
0
,
null
);
// Assert the third sample is read without a format change.
assertReadSample
(
2000
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
);
assertReadSample
(
2000
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
);
}
@Test
...
...
@@ -260,7 +283,14 @@ public final class SampleQueueTest {
// If formatRequired, should read the format rather than the sample.
assertReadFormat
(
true
,
FORMAT_1
);
// Otherwise should read the sample.
assertReadSample
(
1000
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
);
assertReadSample
(
1000
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
);
// Allocation should still be held.
assertAllocationCount
(
1
);
sampleQueue
.
discardToRead
();
...
...
@@ -277,7 +307,14 @@ public final class SampleQueueTest {
// If formatRequired, should read the format rather than the sample.
assertReadFormat
(
true
,
FORMAT_1
);
// Read the sample.
assertReadSample
(
2000
,
false
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
-
1
);
assertReadSample
(
2000
,
/* isKeyFrame= */
false
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
-
1
);
// Allocation should still be held.
assertAllocationCount
(
1
);
sampleQueue
.
discardToRead
();
...
...
@@ -291,7 +328,14 @@ public final class SampleQueueTest {
// If formatRequired, should read the format rather than the sample.
assertReadFormat
(
true
,
FORMAT_1
);
// Read the sample.
assertReadSample
(
3000
,
false
,
/* isEncrypted= */
false
,
DATA
,
ALLOCATION_SIZE
-
1
,
1
);
assertReadSample
(
3000
,
/* isKeyFrame= */
false
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
ALLOCATION_SIZE
-
1
,
1
);
// Allocation should still be held.
assertAllocationCount
(
1
);
sampleQueue
.
discardToRead
();
...
...
@@ -394,11 +438,7 @@ public final class SampleQueueTest {
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isSameInstanceAs
(
mockDrmSession
);
assertReadEncryptedSample
(
/* sampleIndex= */
0
);
...
...
@@ -407,21 +447,13 @@ public final class SampleQueueTest {
assertThat
(
formatHolder
.
drmSession
).
isNull
();
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isNull
();
assertReadEncryptedSample
(
/* sampleIndex= */
2
);
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isSameInstanceAs
(
mockDrmSession
);
}
...
...
@@ -438,11 +470,7 @@ public final class SampleQueueTest {
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isSameInstanceAs
(
mockDrmSession
);
assertReadEncryptedSample
(
/* sampleIndex= */
0
);
...
...
@@ -451,21 +479,13 @@ public final class SampleQueueTest {
assertThat
(
formatHolder
.
drmSession
).
isNull
();
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isSameInstanceAs
(
mockPlaceholderDrmSession
);
assertReadEncryptedSample
(
/* sampleIndex= */
2
);
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
assertThat
(
formatHolder
.
drmSession
).
isSameInstanceAs
(
mockDrmSession
);
assertReadEncryptedSample
(
/* sampleIndex= */
3
);
...
...
@@ -495,11 +515,7 @@ public final class SampleQueueTest {
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
// Fill cryptoInfo.iv with non-zero data. When the 8 byte initialization vector is written into
...
...
@@ -509,11 +525,7 @@ public final class SampleQueueTest {
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_BUFFER_READ
);
// Assert cryptoInfo.iv contains the 8-byte initialization vector and that the trailing 8 bytes
...
...
@@ -608,7 +620,14 @@ public final class SampleQueueTest {
sampleQueue
.
sampleMetadata
(
0
,
C
.
BUFFER_FLAG_KEY_FRAME
,
ALLOCATION_SIZE
,
0
,
null
);
// Once the metadata has been written, check the sample can be read as expected.
assertReadSample
(
0
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
ALLOCATION_SIZE
);
assertReadSample
(
/* timeUs= */
0
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
ALLOCATION_SIZE
);
assertNoSamplesToRead
(
FORMAT_1
);
assertAllocationCount
(
1
);
sampleQueue
.
discardToRead
();
...
...
@@ -641,7 +660,7 @@ public final class SampleQueueTest {
int
skipCount
=
sampleQueue
.
advanceTo
(
LAST_SAMPLE_TIMESTAMP
);
// Should advance to 2nd keyframe (the 4th frame).
assertThat
(
skipCount
).
isEqualTo
(
4
);
assertReadTestData
(
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertReadTestData
(
/* startFormat= */
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertNoSamplesToRead
(
FORMAT_2
);
}
...
...
@@ -651,7 +670,7 @@ public final class SampleQueueTest {
int
skipCount
=
sampleQueue
.
advanceTo
(
LAST_SAMPLE_TIMESTAMP
+
1
);
// Should advance to 2nd keyframe (the 4th frame).
assertThat
(
skipCount
).
isEqualTo
(
4
);
assertReadTestData
(
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertReadTestData
(
/* startFormat= */
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertNoSamplesToRead
(
FORMAT_2
);
}
...
...
@@ -681,7 +700,12 @@ public final class SampleQueueTest {
boolean
success
=
sampleQueue
.
seekTo
(
LAST_SAMPLE_TIMESTAMP
,
false
);
assertThat
(
success
).
isTrue
();
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
4
);
assertReadTestData
(
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertReadTestData
(
/* startFormat= */
null
,
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleCount= */
SAMPLE_TIMESTAMPS
.
length
-
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleOffsetUs= */
0
,
/* decodeOnlyUntilUs= */
LAST_SAMPLE_TIMESTAMP
);
assertNoSamplesToRead
(
FORMAT_2
);
}
...
...
@@ -701,7 +725,12 @@ public final class SampleQueueTest {
boolean
success
=
sampleQueue
.
seekTo
(
LAST_SAMPLE_TIMESTAMP
+
1
,
true
);
assertThat
(
success
).
isTrue
();
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
4
);
assertReadTestData
(
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertReadTestData
(
/* startFormat= */
null
,
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleCount= */
SAMPLE_TIMESTAMPS
.
length
-
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleOffsetUs= */
0
,
/* decodeOnlyUntilUs= */
LAST_SAMPLE_TIMESTAMP
+
1
);
assertNoSamplesToRead
(
FORMAT_2
);
}
...
...
@@ -711,7 +740,13 @@ public final class SampleQueueTest {
boolean
success
=
sampleQueue
.
seekTo
(
LAST_SAMPLE_TIMESTAMP
,
false
);
assertThat
(
success
).
isTrue
();
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
4
);
assertReadTestData
(
null
,
DATA_SECOND_KEYFRAME_INDEX
);
assertReadTestData
(
/* startFormat= */
null
,
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleCount= */
SAMPLE_TIMESTAMPS
.
length
-
DATA_SECOND_KEYFRAME_INDEX
,
/* sampleOffsetUs= */
0
,
/* decodeOnlyUntilUs= */
LAST_SAMPLE_TIMESTAMP
);
assertNoSamplesToRead
(
FORMAT_2
);
// Seek back to the start.
success
=
sampleQueue
.
seekTo
(
SAMPLE_TIMESTAMPS
[
0
],
false
);
...
...
@@ -722,6 +757,51 @@ public final class SampleQueueTest {
}
@Test
public
void
setStartTimeUs_allSamplesAreSyncSamples_discardsOnWriteSide
()
{
// The format uses a MIME type for which MimeTypes.allSamplesAreSyncSamples() is true.
Format
format
=
new
Format
.
Builder
().
setSampleMimeType
(
MimeTypes
.
AUDIO_RAW
).
build
();
Format
[]
sampleFormats
=
new
Format
[
SAMPLE_SIZES
.
length
];
Arrays
.
fill
(
sampleFormats
,
format
);
int
[]
sampleFlags
=
new
int
[
SAMPLE_SIZES
.
length
];
Arrays
.
fill
(
sampleFlags
,
BUFFER_FLAG_KEY_FRAME
);
sampleQueue
.
setStartTimeUs
(
LAST_SAMPLE_TIMESTAMP
);
writeTestData
(
DATA
,
SAMPLE_SIZES
,
SAMPLE_OFFSETS
,
SAMPLE_TIMESTAMPS
,
sampleFormats
,
sampleFlags
);
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
0
);
assertReadFormat
(
/* formatRequired= */
false
,
format
);
assertReadSample
(
SAMPLE_TIMESTAMPS
[
7
],
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
DATA
.
length
-
SAMPLE_OFFSETS
[
7
]
-
SAMPLE_SIZES
[
7
],
SAMPLE_SIZES
[
7
]);
}
@Test
public
void
setStartTimeUs_notAllSamplesAreSyncSamples_discardsOnReadSide
()
{
// The format uses a MIME type for which MimeTypes.allSamplesAreSyncSamples() is false.
Format
format
=
new
Format
.
Builder
().
setSampleMimeType
(
MimeTypes
.
VIDEO_H264
).
build
();
Format
[]
sampleFormats
=
new
Format
[
SAMPLE_SIZES
.
length
];
Arrays
.
fill
(
sampleFormats
,
format
);
sampleQueue
.
setStartTimeUs
(
LAST_SAMPLE_TIMESTAMP
);
writeTestData
();
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
0
);
assertReadTestData
(
/* startFormat= */
null
,
/* firstSampleIndex= */
0
,
/* sampleCount= */
SAMPLE_TIMESTAMPS
.
length
,
/* sampleOffsetUs= */
0
,
/* decodeOnlyUntilUs= */
LAST_SAMPLE_TIMESTAMP
);
}
@Test
public
void
discardToEnd
()
{
writeTestData
();
// Should discard everything.
...
...
@@ -745,7 +825,7 @@ public final class SampleQueueTest {
assertThat
(
sampleQueue
.
getReadIndex
()).
isEqualTo
(
0
);
assertAllocationCount
(
10
);
// Read the first sample.
assertReadTestData
(
null
,
0
,
1
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
1
);
// Shouldn't discard anything.
sampleQueue
.
discardTo
(
SAMPLE_TIMESTAMPS
[
1
]
-
1
,
false
,
true
);
assertThat
(
sampleQueue
.
getFirstIndex
()).
isEqualTo
(
0
);
...
...
@@ -835,7 +915,7 @@ public final class SampleQueueTest {
writeTestData
();
sampleQueue
.
discardUpstreamSamples
(
4
);
assertAllocationCount
(
4
);
assertReadTestData
(
null
,
0
,
4
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
4
);
assertReadFormat
(
false
,
FORMAT_2
);
assertNoSamplesToRead
(
FORMAT_2
);
}
...
...
@@ -843,7 +923,7 @@ public final class SampleQueueTest {
@Test
public
void
discardUpstreamAfterRead
()
{
writeTestData
();
assertReadTestData
(
null
,
0
,
3
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
3
);
sampleQueue
.
discardUpstreamSamples
(
8
);
assertAllocationCount
(
10
);
sampleQueue
.
discardToRead
();
...
...
@@ -908,7 +988,11 @@ public final class SampleQueueTest {
sampleQueue
.
setSampleOffsetUs
(
sampleOffsetUs
);
writeTestData
();
assertReadTestData
(
/* startFormat= */
null
,
/* firstSampleIndex= */
0
,
/* sampleCount= */
8
,
sampleOffsetUs
);
/* startFormat= */
null
,
/* firstSampleIndex= */
0
,
/* sampleCount= */
8
,
sampleOffsetUs
,
/* decodeOnlyUntilUs= */
0
);
assertReadEndOfStream
(
/* formatRequired= */
false
);
}
...
...
@@ -931,6 +1015,7 @@ public final class SampleQueueTest {
assertReadSample
(
unadjustedTimestampUs
+
sampleOffsetUs
,
/* isKeyFrame= */
false
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
...
...
@@ -986,6 +1071,7 @@ public final class SampleQueueTest {
assertReadSample
(
/* timeUs= */
0
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
...
...
@@ -994,6 +1080,7 @@ public final class SampleQueueTest {
assertReadSample
(
/* timeUs= */
1
,
/* isKeyFrame= */
false
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
...
...
@@ -1009,16 +1096,23 @@ public final class SampleQueueTest {
long
spliceSampleTimeUs
=
SAMPLE_TIMESTAMPS
[
4
];
writeFormat
(
FORMAT_SPLICED
);
writeSample
(
DATA
,
spliceSampleTimeUs
,
C
.
BUFFER_FLAG_KEY_FRAME
);
assertReadTestData
(
null
,
0
,
4
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
4
);
assertReadFormat
(
false
,
FORMAT_SPLICED
);
assertReadSample
(
spliceSampleTimeUs
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
DATA
.
length
);
assertReadSample
(
spliceSampleTimeUs
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
DATA
.
length
);
assertReadEndOfStream
(
false
);
}
@Test
public
void
spliceAfterRead
()
{
writeTestData
();
assertReadTestData
(
null
,
0
,
4
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
4
);
sampleQueue
.
splice
();
// Splice should fail, leaving the last 4 samples unchanged.
long
spliceSampleTimeUs
=
SAMPLE_TIMESTAMPS
[
3
];
...
...
@@ -1028,14 +1122,21 @@ public final class SampleQueueTest {
assertReadEndOfStream
(
false
);
sampleQueue
.
seekTo
(
0
);
assertReadTestData
(
null
,
0
,
4
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
4
);
sampleQueue
.
splice
();
// Splice should succeed, replacing the last 4 samples with the sample being written
spliceSampleTimeUs
=
SAMPLE_TIMESTAMPS
[
3
]
+
1
;
writeFormat
(
FORMAT_SPLICED
);
writeSample
(
DATA
,
spliceSampleTimeUs
,
C
.
BUFFER_FLAG_KEY_FRAME
);
assertReadFormat
(
false
,
FORMAT_SPLICED
);
assertReadSample
(
spliceSampleTimeUs
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
DATA
.
length
);
assertReadSample
(
spliceSampleTimeUs
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
DATA
.
length
);
assertReadEndOfStream
(
false
);
}
...
...
@@ -1049,14 +1150,23 @@ public final class SampleQueueTest {
long
spliceSampleTimeUs
=
SAMPLE_TIMESTAMPS
[
4
];
writeFormat
(
FORMAT_SPLICED
);
writeSample
(
DATA
,
spliceSampleTimeUs
,
C
.
BUFFER_FLAG_KEY_FRAME
);
assertReadTestData
(
null
,
0
,
4
,
sampleOffsetUs
);
assertReadTestData
(
/* startFormat= */
null
,
0
,
4
,
sampleOffsetUs
,
/* decodeOnlyUntilUs= */
0
);
assertReadFormat
(
false
,
FORMAT_SPLICED
.
buildUpon
().
setSubsampleOffsetUs
(
sampleOffsetUs
).
build
());
assertReadSample
(
spliceSampleTimeUs
+
sampleOffsetUs
,
true
,
/* isEncrypted= */
false
,
DATA
,
0
,
DATA
.
length
);
spliceSampleTimeUs
+
sampleOffsetUs
,
/* isKeyFrame= */
true
,
/* isDecodeOnly= */
false
,
/* isEncrypted= */
false
,
DATA
,
/* offset= */
0
,
DATA
.
length
);
assertReadEndOfStream
(
false
);
}
@Test
public
void
setStartTime
()
{}
// Internal methods.
/**
...
...
@@ -1119,7 +1229,7 @@ public final class SampleQueueTest {
* Asserts correct reading of standard test data from {@code sampleQueue}.
*/
private
void
assertReadTestData
()
{
assertReadTestData
(
null
,
0
);
assertReadTestData
(
/* startFormat= */
null
,
0
);
}
/**
...
...
@@ -1149,7 +1259,12 @@ public final class SampleQueueTest {
* @param sampleCount The number of samples to read.
*/
private
void
assertReadTestData
(
Format
startFormat
,
int
firstSampleIndex
,
int
sampleCount
)
{
assertReadTestData
(
startFormat
,
firstSampleIndex
,
sampleCount
,
0
);
assertReadTestData
(
startFormat
,
firstSampleIndex
,
sampleCount
,
/* sampleOffsetUs= */
0
,
/* decodeOnlyUntilUs= */
0
);
}
/**
...
...
@@ -1161,7 +1276,11 @@ public final class SampleQueueTest {
* @param sampleOffsetUs The expected sample offset.
*/
private
void
assertReadTestData
(
Format
startFormat
,
int
firstSampleIndex
,
int
sampleCount
,
long
sampleOffsetUs
)
{
Format
startFormat
,
int
firstSampleIndex
,
int
sampleCount
,
long
sampleOffsetUs
,
long
decodeOnlyUntilUs
)
{
Format
format
=
adjustFormat
(
startFormat
,
sampleOffsetUs
);
for
(
int
i
=
firstSampleIndex
;
i
<
firstSampleIndex
+
sampleCount
;
i
++)
{
// Use equals() on the read side despite using referential equality on the write side, since
...
...
@@ -1175,9 +1294,11 @@ public final class SampleQueueTest {
// If we require the format, we should always read it.
assertReadFormat
(
true
,
testSampleFormat
);
// Assert the sample is as expected.
long
expectedTimeUs
=
SAMPLE_TIMESTAMPS
[
i
]
+
sampleOffsetUs
;
assertReadSample
(
SAMPLE_TIMESTAMPS
[
i
]
+
sampleOffset
Us
,
expectedTime
Us
,
(
SAMPLE_FLAGS
[
i
]
&
C
.
BUFFER_FLAG_KEY_FRAME
)
!=
0
,
/* isDecodeOnly= */
expectedTimeUs
<
decodeOnlyUntilUs
,
/* isEncrypted= */
false
,
DATA
,
DATA
.
length
-
SAMPLE_OFFSETS
[
i
]
-
SAMPLE_SIZES
[
i
],
...
...
@@ -1221,12 +1342,7 @@ public final class SampleQueueTest {
private
void
assertReadNothing
(
boolean
formatRequired
)
{
clearFormatHolderAndInputBuffer
();
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_NOTHING_READ
);
// formatHolder should not be populated.
assertThat
(
formatHolder
.
format
).
isNull
();
...
...
@@ -1244,12 +1360,7 @@ public final class SampleQueueTest {
private
void
assertReadEndOfStream
(
boolean
formatRequired
)
{
clearFormatHolderAndInputBuffer
();
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
true
,
/* decodeOnlyUntilUs= */
0
);
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
true
);
assertThat
(
result
).
isEqualTo
(
RESULT_BUFFER_READ
);
// formatHolder should not be populated.
assertThat
(
formatHolder
.
format
).
isNull
();
...
...
@@ -1270,12 +1381,7 @@ public final class SampleQueueTest {
private
void
assertReadFormat
(
boolean
formatRequired
,
Format
format
)
{
clearFormatHolderAndInputBuffer
();
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
formatRequired
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_FORMAT_READ
);
// formatHolder should be populated.
assertThat
(
formatHolder
.
format
).
isEqualTo
(
format
);
...
...
@@ -1292,6 +1398,7 @@ public final class SampleQueueTest {
assertReadSample
(
ENCRYPTED_SAMPLE_TIMESTAMPS
[
sampleIndex
],
isKeyFrame
,
/* isDecodeOnly= */
false
,
isEncrypted
,
sampleData
,
/* offset= */
0
,
...
...
@@ -1304,6 +1411,7 @@ public final class SampleQueueTest {
*
* @param timeUs The expected buffer timestamp.
* @param isKeyFrame The expected keyframe flag.
* @param isDecodeOnly The expected decodeOnly flag.
* @param isEncrypted The expected encrypted flag.
* @param sampleData An array containing the expected sample data.
* @param offset The offset in {@code sampleData} of the expected sample data.
...
...
@@ -1312,6 +1420,7 @@ public final class SampleQueueTest {
private
void
assertReadSample
(
long
timeUs
,
boolean
isKeyFrame
,
boolean
isDecodeOnly
,
boolean
isEncrypted
,
byte
[]
sampleData
,
int
offset
,
...
...
@@ -1319,18 +1428,14 @@ public final class SampleQueueTest {
clearFormatHolderAndInputBuffer
();
int
result
=
sampleQueue
.
read
(
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
inputBuffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
assertThat
(
result
).
isEqualTo
(
RESULT_BUFFER_READ
);
// formatHolder should not be populated.
assertThat
(
formatHolder
.
format
).
isNull
();
// inputBuffer should be populated.
assertThat
(
inputBuffer
.
timeUs
).
isEqualTo
(
timeUs
);
assertThat
(
inputBuffer
.
isKeyFrame
()).
isEqualTo
(
isKeyFrame
);
assertThat
(
inputBuffer
.
isDecodeOnly
()).
is
False
(
);
assertThat
(
inputBuffer
.
isDecodeOnly
()).
is
EqualTo
(
isDecodeOnly
);
assertThat
(
inputBuffer
.
isEncrypted
()).
isEqualTo
(
isEncrypted
);
inputBuffer
.
flip
();
assertThat
(
inputBuffer
.
data
.
limit
()).
isEqualTo
(
length
);
...
...
library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java
View file @
8bd01a7b
...
...
@@ -380,11 +380,7 @@ public final class PlayerEmsgHandler implements Handler.Callback {
buffer
.
clear
();
int
result
=
sampleQueue
.
read
(
formatHolder
,
buffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
,
/* decodeOnlyUntilUs= */
0
);
formatHolder
,
buffer
,
/* formatRequired= */
false
,
/* loadingFinished= */
false
);
if
(
result
==
C
.
RESULT_BUFFER_READ
)
{
buffer
.
flip
();
return
buffer
;
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
View file @
8bd01a7b
...
...
@@ -560,8 +560,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
int
result
=
sampleQueues
[
sampleQueueIndex
].
read
(
formatHolder
,
buffer
,
requireFormat
,
loadingFinished
,
lastSeekPositionUs
);
sampleQueues
[
sampleQueueIndex
].
read
(
formatHolder
,
buffer
,
requireFormat
,
loadingFinished
);
if
(
result
==
C
.
RESULT_FORMAT_READ
)
{
Format
format
=
Assertions
.
checkNotNull
(
formatHolder
.
format
);
if
(
sampleQueueIndex
==
primarySampleQueueIndex
)
{
...
...
@@ -641,6 +640,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
if
(
isPendingReset
())
{
chunkQueue
=
Collections
.
emptyList
();
loadPositionUs
=
pendingResetPositionUs
;
for
(
SampleQueue
sampleQueue
:
sampleQueues
)
{
sampleQueue
.
setStartTimeUs
(
pendingResetPositionUs
);
}
}
else
{
chunkQueue
=
readOnlyMediaChunks
;
HlsMediaChunk
lastMediaChunk
=
getLastMediaChunk
();
...
...
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