Skip to content

Commit

Permalink
Create chunks from parts in HlsChunkSource
Browse files Browse the repository at this point in the history
Issue: #5011
PiperOrigin-RevId: 342022947
  • Loading branch information
marcbaechinger authored and christosts committed Nov 13, 2020
1 parent 2693a10 commit e3c725a
Show file tree
Hide file tree
Showing 7 changed files with 535 additions and 92 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
* @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.
Expand All @@ -79,7 +79,7 @@ public static HlsMediaChunk createInstance(
Format format,
long startOfPlaylistInPeriodUs,
HlsMediaPlaylist mediaPlaylist,
int segmentIndexInPlaylist,
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
Uri playlistUrl,
@Nullable List<Format> muxedCaptionFormats,
int trackSelectionReason,
Expand All @@ -90,7 +90,7 @@ public static HlsMediaChunk createInstance(
@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),
Expand Down Expand Up @@ -136,10 +136,10 @@ public static HlsMediaChunk createInstance(
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
Expand All @@ -152,7 +152,6 @@ public static HlsMediaChunk createInstance(
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false;
}

return new HlsMediaChunk(
extractorFactory,
mediaDataSource,
Expand All @@ -168,7 +167,8 @@ public static HlsMediaChunk createInstance(
trackSelectionData,
segmentStartTimeInPeriodUs,
segmentEndTimeInPeriodUs,
/* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,
segmentBaseHolder.mediaSequence,
segmentBaseHolder.partIndex,
discontinuitySequenceNumber,
mediaSegment.hasGapTag,
isMasterTimestampSource,
Expand Down Expand Up @@ -201,6 +201,9 @@ public static HlsMediaChunk createInstance(
/** 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;
Expand Down Expand Up @@ -243,6 +246,7 @@ private HlsMediaChunk(
long startTimeUs,
long endTimeUs,
long chunkMediaSequence,
int partIndex,
int discontinuitySequenceNumber,
boolean hasGapTag,
boolean isMasterTimestampSource,
Expand All @@ -262,6 +266,7 @@ private HlsMediaChunk(
endTimeUs,
chunkMediaSequence);
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
this.partIndex = partIndex;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec;
this.initDataSource = initDataSource;
Expand Down Expand Up @@ -541,4 +546,13 @@ private static DataSource buildDataSource(
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,9 @@ private static HlsMediaPlaylist parseMediaPlaylist(
}
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(
Expand All @@ -811,21 +811,24 @@ private static HlsMediaPlaylist parseMediaPlaylist(
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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* 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;
}
}
Loading

0 comments on commit e3c725a

Please sign in to comment.