Commit e3c725aa by bachinger Committed by Christos Tsilopoulos

Create chunks from parts in HlsChunkSource

Issue: #5011
PiperOrigin-RevId: 342022947
parent 2693a107
......@@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param format The chunk format.
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
* @param mediaPlaylist The media playlist from which this chunk was obtained.
* @param segmentIndexInPlaylist The index of the segment in the media playlist.
* @param segmentBaseHolder The segment holder.
* @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
* information is available in the master playlist.
......@@ -79,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Format format,
long startOfPlaylistInPeriodUs,
HlsMediaPlaylist mediaPlaylist,
int segmentIndexInPlaylist,
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
Uri playlistUrl,
@Nullable List<Format> muxedCaptionFormats,
int trackSelectionReason,
......@@ -90,7 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey) {
// Media segment.
HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
DataSpec dataSpec =
new DataSpec(
UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
......@@ -136,10 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
id3Decoder = previousChunk.id3Decoder;
scratchId3Data = previousChunk.scratchId3Data;
boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist);
boolean canContinueWithoutSplice =
isFollowingChunk
|| (mediaPlaylist.hasIndependentSegments
&& segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
shouldSpliceIn = !canContinueWithoutSplice;
previousExtractor =
isFollowingChunk
......@@ -152,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false;
}
return new HlsMediaChunk(
extractorFactory,
mediaDataSource,
......@@ -168,7 +167,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
trackSelectionData,
segmentStartTimeInPeriodUs,
segmentEndTimeInPeriodUs,
/* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,
segmentBaseHolder.mediaSequence,
segmentBaseHolder.partIndex,
discontinuitySequenceNumber,
mediaSegment.hasGapTag,
isMasterTimestampSource,
......@@ -201,6 +201,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Whether samples for this chunk should be spliced into existing samples. */
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 DataSpec initDataSpec;
@Nullable private final HlsMediaChunkExtractor previousExtractor;
......@@ -243,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
long startTimeUs,
long endTimeUs,
long chunkMediaSequence,
int partIndex,
int discontinuitySequenceNumber,
boolean hasGapTag,
boolean isMasterTimestampSource,
......@@ -262,6 +266,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
endTimeUs,
chunkMediaSequence);
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
this.partIndex = partIndex;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec;
this.initDataSource = initDataSource;
......@@ -541,4 +546,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
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;
}
}
......@@ -797,9 +797,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long byteRangeStart =
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ 0);
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ C.LENGTH_UNSET);
long byteRangeLength =
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.TIME_UNSET);
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.LENGTH_UNSET);
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
......@@ -811,21 +811,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
preloadPart =
new Part(
url,
initializationSegment,
/* durationUs= */ 0,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
byteRangeStart,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false,
/* isPreload= */ true);
if (byteRangeStart == C.LENGTH_UNSET || byteRangeLength != C.LENGTH_UNSET) {
// Skip preload part if it is an unbounded range request.
preloadPart =
new Part(
url,
initializationSegment,
/* durationUs= */ 0,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
byteRangeStart != C.LENGTH_UNSET ? byteRangeStart : 0,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false,
/* isPreload= */ true);
}
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
......
/*
* 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;
}
}
......@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.Iterables;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
......@@ -362,6 +363,7 @@ public class HlsMediaPlaylistParserTest {
assertThat(secondPart.byteRangeOffset).isEqualTo(1234);
// Assert trailing parts.
HlsMediaPlaylist.Part thirdPart = playlist.trailingParts.get(0);
// Assert tailing parts.
assertThat(thirdPart.byteRangeLength).isEqualTo(1000);
assertThat(thirdPart.byteRangeOffset).isEqualTo(1234);
assertThat(thirdPart.relativeStartTimeUs).isEqualTo(8_000_000);
......@@ -545,6 +547,27 @@ public class HlsMediaPlaylistParserTest {
}
@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()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
......
#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"
#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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment