/*
 * Copyright (C) 2016 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 androidx.media3.exoplayer.hls.playlist;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import android.net.Uri;
import android.util.Base64;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
import androidx.media3.extractor.mp4.PsshAtomUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.Iterables;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Test for {@link HlsMediaPlaylistParserTest}. */
@RunWith(AndroidJUnit4.class)
public class HlsMediaPlaylistParserTest {

  @Test
  public void parseMediaPlaylist() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-PLAYLIST-TYPE:VOD\n"
            + "#EXT-X-START:TIME-OFFSET=-25\n"
            + "#EXT-X-TARGETDURATION:8\n"
            + "#EXT-X-MEDIA-SEQUENCE:2679\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
            + "#EXT-X-ALLOW-CACHE:YES\n"
            + "\n"
            + "#EXTINF:7.975,\n"
            + "#EXT-X-BYTERANGE:51370@0\n"
            + "https://priv.example.com/fileSequence2679.ts\n"
            + "\n"
            + "#EXT-X-KEY:METHOD=AES-128,"
            + "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
            + "#EXTINF:7.975,segment title\n"
            + "#EXT-X-BYTERANGE:51501@2147483648\n"
            + "https://priv.example.com/fileSequence2680.ts\n"
            + "\n"
            + "#EXT-X-KEY:METHOD=NONE\n"
            + "#EXTINF:7.941,segment title .,:/# with interesting chars\n"
            + "#EXT-X-BYTERANGE:51501\n" // @2147535149
            + "https://priv.example.com/fileSequence2681.ts\n"
            + "\n"
            + "#EXT-X-DISCONTINUITY\n"
            + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
            + "#EXTINF:7.975\n" // Trailing comma is omitted.
            + "#EXT-X-BYTERANGE:51740\n" // @2147586650
            + "https://priv.example.com/fileSequence2682.ts\n"
            + "\n"
            + "#EXTINF:7.975,\n"
            + "https://priv.example.com/fileSequence2683.ts\n"
            + "\n"
            // 2.002 tests correct rounding, see https://github.com/google/ExoPlayer/issues/9575.
            + "#EXTINF:2.002,\n"
            + "https://priv.example.com/fileSequence2684.ts\n"
            + "#EXT-X-ENDLIST";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);

    HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
    assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD);
    assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000);

    assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679);
    assertThat(mediaPlaylist.version).isEqualTo(3);
    assertThat(mediaPlaylist.hasEndTag).isTrue();
    assertThat(mediaPlaylist.protectionSchemes).isNull();
    assertThat(mediaPlaylist.targetDurationUs).isEqualTo(8000000);
    assertThat(mediaPlaylist.partTargetDurationUs).isEqualTo(C.TIME_UNSET);
    List<Segment> segments = mediaPlaylist.segments;
    assertThat(segments).isNotNull();
    assertThat(segments).hasSize(6);

    Segment segment = segments.get(0);
    assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence)
        .isEqualTo(4);
    assertThat(segment.durationUs).isEqualTo(7975000);
    assertThat(segment.title).isEqualTo("");
    assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
    assertThat(segment.encryptionIV).isNull();
    assertThat(segment.byteRangeLength).isEqualTo(51370);
    assertThat(segment.byteRangeOffset).isEqualTo(0);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts");

    segment = segments.get(1);
    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
    assertThat(segment.durationUs).isEqualTo(7975000);
    assertThat(segment.title).isEqualTo("segment title");
    assertThat(segment.fullSegmentEncryptionKeyUri)
        .isEqualTo("https://priv.example.com/key.php?r=2680");
    assertThat(segment.encryptionIV).isEqualTo("0x1566B");
    assertThat(segment.byteRangeLength).isEqualTo(51501);
    assertThat(segment.byteRangeOffset).isEqualTo(2147483648L);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts");

    segment = segments.get(2);
    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
    assertThat(segment.durationUs).isEqualTo(7941000);
    assertThat(segment.title).isEqualTo("segment title .,:/# with interesting chars");
    assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
    assertThat(segment.encryptionIV).isEqualTo(null);
    assertThat(segment.byteRangeLength).isEqualTo(51501);
    assertThat(segment.byteRangeOffset).isEqualTo(2147535149L);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts");

    segment = segments.get(3);
    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(segment.durationUs).isEqualTo(7975000);
    assertThat(segment.title).isEqualTo("");
    assertThat(segment.fullSegmentEncryptionKeyUri)
        .isEqualTo("https://priv.example.com/key.php?r=2682");
    // 0xA7A == 2682.
    assertThat(segment.encryptionIV).isNotNull();
    assertThat(segment.encryptionIV).ignoringCase().isEqualTo("A7A");
    assertThat(segment.byteRangeLength).isEqualTo(51740);
    assertThat(segment.byteRangeOffset).isEqualTo(2147586650L);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts");

    segment = segments.get(4);
    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(segment.durationUs).isEqualTo(7975000);
    assertThat(segment.title).isEqualTo("");
    assertThat(segment.fullSegmentEncryptionKeyUri)
        .isEqualTo("https://priv.example.com/key.php?r=2682");
    // 0xA7B == 2683.
    assertThat(segment.encryptionIV).isNotNull();
    assertThat(segment.encryptionIV).ignoringCase().isEqualTo("A7B");
    assertThat(segment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
    assertThat(segment.byteRangeOffset).isEqualTo(0);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");

    segment = segments.get(5);
    assertThat(segment.durationUs).isEqualTo(2002000);
  }

  @Test
  public void parseMediaPlaylist_withByteRanges() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "\n"
            + "#EXT-X-BYTERANGE:200@100\n"
            + "#EXT-X-MAP:URI=\"stream.mp4\"\n"
            + "#EXTINF:5,\n"
            + "#EXT-X-BYTERANGE:400\n"
            + "stream.mp4\n"
            + "#EXTINF:5,\n"
            + "#EXT-X-BYTERANGE:500\n"
            + "stream.mp4\n"
            + "#EXT-X-DISCONTINUITY\n"
            + "#EXT-X-MAP:URI=\"init.mp4\"\n"
            + "#EXTINF:5,\n"
            + "segment.mp4\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);

    HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
    List<Segment> segments = mediaPlaylist.segments;

    assertThat(segments).isNotNull();
    assertThat(segments).hasSize(3);

    Segment segment = segments.get(0);
    assertThat(segment.initializationSegment.byteRangeOffset).isEqualTo(100);
    assertThat(segment.initializationSegment.byteRangeLength).isEqualTo(200);
    assertThat(segment.byteRangeOffset).isEqualTo(300);
    assertThat(segment.byteRangeLength).isEqualTo(400);

    segment = segments.get(1);
    assertThat(segment.byteRangeOffset).isEqualTo(700);
    assertThat(segment.byteRangeLength).isEqualTo(500);

    segment = segments.get(2);
    assertThat(segment.initializationSegment.byteRangeOffset).isEqualTo(0);
    assertThat(segment.initializationSegment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
    assertThat(segment.byteRangeOffset).isEqualTo(0);
    assertThat(segment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
  }

  @Test
  public void parseSampleAesMethod() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-MEDIA-SEQUENCE:0\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/1.ts\n"
            + "\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,URI="
            + "\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\","
            + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\","
            + "KEYFORMAT=\"com.widevine\",IV=0x1566B\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/2.ts\n"
            + "#EXT-X-ENDLIST\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs);
    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();

    assertThat(playlist.segments.get(0).drmInitData).isNull();

    assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue();
  }

  @Test
  public void parseSampleAesCencMethod() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-MEDIA-SEQUENCE:0\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/1.ts\n"
            + "\n"
            + "#EXT-X-KEY:URI=\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\","
            + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\","
            + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\","
            + "IV=0x1566B,METHOD=SAMPLE-AES-CENC \n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/2.ts\n"
            + "#EXT-X-ENDLIST\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);
    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();
  }

  @Test
  public void parseSampleAesCtrMethod() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-MEDIA-SEQUENCE:0\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/1.ts\n"
            + "\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="
            + "\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\","
            + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\","
            + "KEYFORMAT=\"com.widevine\",IV=0x1566B\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/2.ts\n"
            + "#EXT-X-ENDLIST\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);
    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();
  }

  @Test
  public void parseMediaPlaylist_withPartMediaInformation_succeeds() 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-SERVER-CONTROL:PART-HOLD-BACK=1.234\n"
            + "#EXT-X-PART-INF:PART-TARGET=0.5\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z\n"
            + "#EXT-X-MAP:URI=\"init.mp4\"\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence266.mp4";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.partTargetDurationUs).isEqualTo(500000);
  }

  @Test
  public void parseMediaPlaylist_withoutServerControl_serverControlDefaultValues()
      throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-MEDIA-SEQUENCE:0\n"
            + "#EXTINF:8,\n"
            + "https://priv.example.com/1.ts\n"
            + "#EXT-X-ENDLIST\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(playlist.serverControl.canBlockReload).isFalse();
    assertThat(playlist.serverControl.partHoldBackUs).isEqualTo(C.TIME_UNSET);
    assertThat(playlist.serverControl.holdBackUs).isEqualTo(C.TIME_UNSET);
    assertThat(playlist.serverControl.skipUntilUs).isEqualTo(C.TIME_UNSET);
    assertThat(playlist.serverControl.canSkipDateRanges).isFalse();
  }

  @Test
  public void parseMediaPlaylist_withServerControl_succeeds() 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-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=18.5,PART-HOLD-BACK=1.234,"
            + "CAN-SKIP-UNTIL=24.0,CAN-SKIP-DATERANGES=YES\n"
            + "#EXT-X-PART-INF:PART-TARGET=0.5\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z\n"
            + "#EXT-X-MAP:URI=\"init.mp4\"\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence266.mp4";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.serverControl.canBlockReload).isTrue();
    assertThat(playlist.serverControl.partHoldBackUs).isEqualTo(1234000);
    assertThat(playlist.serverControl.holdBackUs).isEqualTo(18500000);
    assertThat(playlist.serverControl.skipUntilUs).isEqualTo(24000000);
    assertThat(playlist.serverControl.canSkipDateRanges).isTrue();
  }

  @Test
  public void parseMediaPlaylist_withSkippedSegments_correctlyMergedSegments() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String previousPlaylistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
            + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
            + "#EXT-X-MEDIA-SEQUENCE:263\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence264.mp4\n"
            + "#EXT-X-DISCONTINUITY\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence265.mp4\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence266.mp4";
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
            + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
            + "#EXT-X-MEDIA-SEQUENCE:265\n"
            + "#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence266.mp4"
            + "#EXTINF:4.00008,\n"
            + "fileSequence267.mp4\n";
    InputStream previousInputStream =
        new ByteArrayInputStream(Util.getUtf8Bytes(previousPlaylistString));
    HlsMediaPlaylist previousPlaylist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, previousInputStream);
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist)
            new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist)
                .parse(playlistUri, inputStream);

    assertThat(playlist.segments).hasSize(3);
    assertThat(playlist.segments.get(1).relativeStartTimeUs).isEqualTo(4000080);
    assertThat(previousPlaylist.segments.get(0).relativeDiscontinuitySequence).isEqualTo(0);
    assertThat(previousPlaylist.segments.get(1).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(previousPlaylist.segments.get(2).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(0).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(1).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(2).relativeDiscontinuitySequence).isEqualTo(1);
  }

  @Test
  public void parseMediaPlaylist_withSkippedSegments_correctlyMergedParts() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String previousPlaylistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
            + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
            + "#EXT-X-MEDIA-SEQUENCE:264\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part264.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part264.2.ts\"\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence264.mp4\n"
            + "#EXT-X-DISCONTINUITY\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part265.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part265.2.ts\"\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence265.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.2.ts\"\n"
            + "#EXTINF:4.00008,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"";
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
            + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
            + "#EXT-X-MEDIA-SEQUENCE:265\n"
            + "#EXT-X-SKIP:SKIPPED-SEGMENTS=2\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"";
    InputStream previousInputStream =
        new ByteArrayInputStream(Util.getUtf8Bytes(previousPlaylistString));
    HlsMediaPlaylist previousPlaylist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, previousInputStream);
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist)
            new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist)
                .parse(playlistUri, inputStream);

    assertThat(playlist.segments).hasSize(2);
    assertThat(playlist.segments.get(0).relativeStartTimeUs).isEqualTo(0);
    assertThat(playlist.segments.get(0).parts.get(0).relativeStartTimeUs).isEqualTo(0);
    assertThat(playlist.segments.get(0).parts.get(0).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(0).parts.get(1).relativeStartTimeUs).isEqualTo(2000000);
    assertThat(playlist.segments.get(0).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(1).relativeStartTimeUs).isEqualTo(4000080);
    assertThat(playlist.segments.get(1).parts.get(0).relativeStartTimeUs).isEqualTo(4000080);
    assertThat(playlist.segments.get(1).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.segments.get(1).parts.get(1).relativeStartTimeUs).isEqualTo(6000080);
    assertThat(playlist.segments.get(1).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
    assertThat(playlist.trailingParts.get(0).relativeStartTimeUs).isEqualTo(8000160);
    assertThat(playlist.trailingParts.get(0).relativeDiscontinuitySequence).isEqualTo(1);
  }

  @Test
  public void parseMediaPlaylist_withParts_parsesPartWithAllAttributes() 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"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,GAP=YES,"
            + "INDEPENDENT=YES,URI=\"part267.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,BYTERANGE=\"1000@1234\",URI=\"part267.2.ts\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence267.ts\n"
            + "#EXT-X-PART:DURATION=2.00000, BYTERANGE=\"1000@1234\",URI=\"part268.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part268.2.ts\", BYTERANGE=\"1000\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.segments.get(1).parts).hasSize(2);
    assertThat(playlist.trailingParts).hasSize(2);

    HlsMediaPlaylist.Part firstPart = playlist.segments.get(1).parts.get(0);
    assertThat(firstPart.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
    assertThat(firstPart.byteRangeOffset).isEqualTo(0);
    assertThat(firstPart.durationUs).isEqualTo(2_000_000);
    assertThat(firstPart.relativeStartTimeUs).isEqualTo(playlist.segments.get(0).durationUs);
    assertThat(firstPart.isIndependent).isTrue();
    assertThat(firstPart.isPreload).isFalse();
    assertThat(firstPart.hasGapTag).isTrue();
    assertThat(firstPart.url).isEqualTo("part267.1.ts");
    HlsMediaPlaylist.Part secondPart = playlist.segments.get(1).parts.get(1);
    assertThat(secondPart.byteRangeLength).isEqualTo(1000);
    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);
    HlsMediaPlaylist.Part lastPart = playlist.trailingParts.get(1);
    assertThat(lastPart.relativeStartTimeUs).isEqualTo(10_000_000);
    assertThat(lastPart.hasGapTag).isFalse();
    assertThat(lastPart.isIndependent).isFalse();
    assertThat(lastPart.byteRangeLength).isEqualTo(1000);
    assertThat(lastPart.byteRangeOffset).isEqualTo(2234);
  }

  @Test
  public void parseMediaPlaylist_withPartAndAesPlayReadyKey_correctDrmInitData()
      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-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"com.microsoft.playready\","
            + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.ts\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo("cbcs");
    HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
    assertThat(part.drmInitData.schemeType).isEqualTo("cbcs");
    assertThat(part.drmInitData.schemeDataCount).isEqualTo(1);
    assertThat(part.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
    assertThat(part.drmInitData.get(0).data)
        .isEqualTo(
            PsshAtomUtil.buildPsshAtom(
                C.PLAYREADY_UUID,
                Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
  }

  @Test
  public void parseMediaPlaylist_withPartAndAesPlayReadyWithOutPrecedingSegment_correctDrmInitData()
      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-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"com.microsoft.playready\","
            + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments).isEmpty();
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo("cbcs");
    HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
    assertThat(part.drmInitData.schemeType).isEqualTo("cbcs");
    assertThat(part.drmInitData.schemeDataCount).isEqualTo(1);
    assertThat(part.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
    assertThat(part.drmInitData.get(0).data)
        .isEqualTo(
            PsshAtomUtil.buildPsshAtom(
                C.PLAYREADY_UUID,
                Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
  }

  @Test
  public void parseMediaPlaylist_withPartAndAes128_partHasDrmKeyUriAndIV() 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-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
            + ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.ts\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
    assertThat(playlist.protectionSchemes).isNull();
    assertThat(part.drmInitData).isNull();
    assertThat(part.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
    assertThat(part.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
  }

  @Test
  public void parseMediaPlaylist_withPartAndAes128WithoutPrecedingSegment_partHasDrmKeyUriAndIV()
      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-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
            + ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments).isEmpty();
    HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
    assertThat(playlist.protectionSchemes).isNull();
    assertThat(part.drmInitData).isNull();
    assertThat(part.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
    assertThat(part.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
  }

  @Test
  public void parseMediaPlaylist_withPreloadHintTypePart_hasPreloadPartWithAllAttributes()
      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-MAP:URI=\"map.mp4\"\n"
            + "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
            + ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,BYTERANGE-START=1234,BYTERANGE-LENGTH=1000,"
            + "URI=\"filePart267.2.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.trailingParts).hasSize(2);
    HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(1);
    assertThat(preloadPart.durationUs).isEqualTo(0L);
    assertThat(preloadPart.url).isEqualTo("filePart267.2.ts");
    assertThat(preloadPart.byteRangeLength).isEqualTo(1000);
    assertThat(preloadPart.byteRangeOffset).isEqualTo(1234);
    assertThat(preloadPart.initializationSegment.url).isEqualTo("map.mp4");
    assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
    assertThat(preloadPart.isPreload).isTrue();
  }

  @Test
  public void parseMediaPlaylist_withMultiplePreloadHintTypeParts_picksOnlyFirstPreloadPart()
      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\"\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.3.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.trailingParts).hasSize(2);
    assertThat(playlist.trailingParts.get(1).url).isEqualTo("filePart267.2.ts");
    assertThat(playlist.trailingParts.get(1).isPreload).isTrue();
  }

  @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");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"com.microsoft.playready\","
            + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.trailingParts).hasSize(1);
    assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(1);
    HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
    assertThat(preloadPart.drmInitData.schemeType).isEqualTo("cbcs");
    assertThat(preloadPart.drmInitData.schemeDataCount).isEqualTo(1);
    assertThat(preloadPart.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
    assertThat(preloadPart.drmInitData.get(0).data)
        .isEqualTo(
            PsshAtomUtil.buildPsshAtom(
                C.PLAYREADY_UUID,
                Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
  }

  @Test
  public void parseMediaPlaylist_withPreloadHintTypePartAndNewAesPlayReadyKey_correctDrmInitData()
      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"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"com.microsoft.playready\","
            + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.trailingParts).hasSize(1);
    assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(1);
    HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
    assertThat(preloadPart.drmInitData.schemeType).isEqualTo("cbcs");
    assertThat(preloadPart.drmInitData.schemeDataCount).isEqualTo(1);
    assertThat(preloadPart.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
    assertThat(preloadPart.drmInitData.get(0).data)
        .isEqualTo(
            PsshAtomUtil.buildPsshAtom(
                C.PLAYREADY_UUID,
                Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
  }

  @Test
  public void parseMediaPlaylist_withPreloadHintTypePartAndAes128_partHasDrmKeyUriAndIV()
      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"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
            + ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.segments.get(0).parts).isEmpty();
    assertThat(playlist.trailingParts).hasSize(1);
    HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
    assertThat(preloadPart.drmInitData).isNull();
    assertThat(preloadPart.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
    assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
  }

  @Test
  public void parseMediaPlaylist_withRenditionReportWithoutPartTargetDuration_lastPartIndexUnset()
      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"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(100);
    assertThat(report0.lastPartIndex).isEqualTo(C.INDEX_UNSET);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportWithoutPartTargetDurationWithoutLastMsn_sameLastMsnAsCurrentPlaylist()
          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"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(266);
    assertThat(report0.lastPartIndex).isEqualTo(C.INDEX_UNSET);
  }

  @Test
  public void parseMediaPlaylist_withRenditionReportLowLatency_parseAllAttributes()
      throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100,LAST-PART=2\n"
            + "#EXT-X-RENDITION-REPORT:"
            + "URI=\"http://foo.bar/rendition2.m3u8\",LAST-MSN=1000,LAST-PART=3\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(2);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(100);
    assertThat(report0.lastPartIndex).isEqualTo(2);
    HlsMediaPlaylist.RenditionReport report2 =
        playlist.renditionReports.get(Uri.parse("http://foo.bar/rendition2.m3u8"));
    assertThat(report2.lastMediaSequence).isEqualTo(1000);
    assertThat(report2.lastPartIndex).isEqualTo(3);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastMsn_sameMsnAsCurrentPlaylist()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-PART=2\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(267);
    assertThat(report0.lastPartIndex).isEqualTo(2);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportBeforeSegmentsWithoutPartTargetDurationWithoutLastMsn_sameLastMsnAsCurrentPlaylist()
          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-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report.lastMediaSequence).isEqualTo(266);
    assertThat(report.lastPartIndex).isEqualTo(C.INDEX_UNSET);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportBeforeSegementsDefaultMsn_sameMsnAsCurrentPlaylist()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-PART=2\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report.lastMediaSequence).isEqualTo(267);
    assertThat(report.lastPartIndex).isEqualTo(2);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportBeforeSegementsDefaultLastPart_sameLastPartIndexAsCurrentPlaylist()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=267\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report.lastMediaSequence).isEqualTo(267);
    assertThat(report.lastPartIndex).isEqualTo(1);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportBeforeSegements_sameMsnAndLastPartIndexAsCurrentPlaylist()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report.lastMediaSequence).isEqualTo(267);
    assertThat(report.lastPartIndex).isEqualTo(1);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastPartIndex_sameLastPartIndexAsCurrentPlaylist()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=267\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(267);
    assertThat(report0.lastPartIndex).isEqualTo(0);
  }

  @Test
  public void
      parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastPartIndex_ignoredPreloadPart()
          throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part267.0.ts\"\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.1.ts\"\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=267\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.trailingParts).hasSize(2);
    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(267);
    assertThat(report0.lastPartIndex).isEqualTo(0);
  }

  @Test
  public void parseMediaPlaylist_withRenditionReportLowLatencyFullSegment_rollingPartIndexUriParam()
      throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:4\n"
            + "#EXT-X-PART-INF:PART-TARGET=1\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:266\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.0.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.1.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.2.ts\"\n"
            + "#EXT-X-PART:DURATION=2.00000,URI=\"part266.3.ts\"\n"
            + "#EXTINF:4.00000,\n"
            + "fileSequence266.mp4\n"
            + "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.0.ts\"\n"
            + "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.renditionReports).hasSize(1);
    HlsMediaPlaylist.RenditionReport report0 =
        playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
    assertThat(report0.lastMediaSequence).isEqualTo(266);
    assertThat(report0.lastPartIndex).isEqualTo(3);
  }

  @Test
  public void multipleExtXKeysForSingleSegment() throws Exception {
    Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-TARGETDURATION:6\n"
            + "#EXT-X-MAP:URI=\"map.mp4\"\n"
            + "#EXTINF:5.005,\n"
            + "s000000.mp4\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\","
            + "KEYFORMATVERSIONS=\"1\","
            + "URI=\"data:text/plain;base64,Tm90aGluZyB0byBzZWUgaGVyZQ==\"\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\","
            + "KEYFORMATVERSIONS=\"1\","
            + "URI=\"data:text/plain;charset=UTF-16;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\","
            + "KEYFORMATVERSIONS=\"1\",URI=\"skd://QW5vdGhlciBlYXN0ZXIgZWdn\"\n"
            + "#EXT-X-MAP:URI=\"map.mp4\"\n"
            + "#EXTINF:5.005,\n"
            + "s000000.mp4\n"
            + "#EXTINF:5.005,\n"
            + "s000001.mp4\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,"
            + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\","
            + "KEYFORMATVERSIONS=\"1\","
            + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\""
            + "\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\","
            + "KEYFORMATVERSIONS=\"1\","
            + "URI=\"data:text/plain;charset=UTF-16;base64,T2ssIGl0J3Mgbm90IGZ1biBhbnltb3Jl\"\n"
            + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\","
            + "KEYFORMATVERSIONS=\"1\","
            + "URI=\"skd://V2FpdCB1bnRpbCB5b3Ugc2VlIHRoZSBuZXh0IG9uZSE=\"\n"
            + "#EXTINF:5.005,\n"
            + "s000024.mp4\n"
            + "#EXTINF:5.005,\n"
            + "s000025.mp4\n"
            + "#EXT-X-KEY:METHOD=NONE\n"
            + "#EXTINF:5.005,\n"
            + "s000026.mp4\n"
            + "#EXTINF:5.005,\n"
            + "s000026.mp4\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs);
    // Unsupported protection schemes like com.apple.streamingkeydelivery are ignored.
    assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(2);
    assertThat(playlist.protectionSchemes.get(0).matches(C.PLAYREADY_UUID)).isTrue();
    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();
    assertThat(playlist.protectionSchemes.get(1).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.protectionSchemes.get(1).hasData()).isFalse();

    assertThat(playlist.segments.get(0).drmInitData).isNull();

    assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue();
    assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue();
    assertThat(playlist.segments.get(1).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.segments.get(1).drmInitData.get(1).hasData()).isTrue();

    assertThat(playlist.segments.get(1).drmInitData)
        .isEqualTo(playlist.segments.get(2).drmInitData);
    assertThat(playlist.segments.get(2).drmInitData)
        .isNotEqualTo(playlist.segments.get(3).drmInitData);
    assertThat(playlist.segments.get(3).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue();
    assertThat(playlist.segments.get(3).drmInitData.get(0).hasData()).isTrue();
    assertThat(playlist.segments.get(3).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue();
    assertThat(playlist.segments.get(3).drmInitData.get(1).hasData()).isTrue();

    assertThat(playlist.segments.get(3).drmInitData)
        .isEqualTo(playlist.segments.get(4).drmInitData);
    assertThat(playlist.segments.get(5).drmInitData).isNull();
    assertThat(playlist.segments.get(6).drmInitData).isNull();
  }

  @Test
  public void gapTag() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test2.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-PLAYLIST-TYPE:VOD\n"
            + "#EXT-X-MEDIA-SEQUENCE:0\n"
            + "#EXT-X-PROGRAM-DATE-TIME:2016-09-22T02:00:01+00:00\n"
            + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://example.com/key?value=something\"\n"
            + "#EXTINF:5.005,\n"
            + "02/00/27.ts\n"
            + "#EXTINF:5.005,\n"
            + "02/00/32.ts\n"
            + "#EXT-X-KEY:METHOD=NONE\n"
            + "#EXTINF:5.005,\n"
            + "#EXT-X-GAP \n"
            + "../test.ts\n"
            + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://key-service.bamgrid.com/1.0/key?"
            + "hex-value=9FB8989D15EEAAF8B21B860D7ED3072A\",IV=0x410C8AC18AA42EFA18B5155484F5FC34\n"
            + "#EXTINF:5.005,\n"
            + "02/00/42.ts\n"
            + "#EXTINF:5.005,\n"
            + "02/00/47.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    assertThat(playlist.hasEndTag).isFalse();
    assertThat(playlist.segments.get(1).hasGapTag).isFalse();
    assertThat(playlist.segments.get(2).hasGapTag).isTrue();
    assertThat(playlist.segments.get(3).hasGapTag).isFalse();
  }

  @Test
  public void mapTag() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXTINF:5.005,\n"
            + "02/00/27.ts\n"
            + "#EXT-X-MAP:URI=\"init1.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/32.ts\n"
            + "#EXTINF:5.005,\n"
            + "02/00/42.ts\n"
            + "#EXT-X-MAP:URI=\"init2.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/47.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    List<Segment> segments = playlist.segments;
    assertThat(segments.get(0).initializationSegment).isNull();
    assertThat(segments.get(1).initializationSegment)
        .isSameInstanceAs(segments.get(2).initializationSegment);
    assertThat(segments.get(1).initializationSegment.url).isEqualTo("init1.ts");
    assertThat(segments.get(3).initializationSegment.url).isEqualTo("init2.ts");
  }

  @Test
  public void noExplicitInitSegmentInIFrameOnly_infersInitSegment() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-I-FRAMES-ONLY\n"
            + "#EXTINF:5.005,\n"
            + "#EXT-X-BYTERANGE:100@300\n"
            + "segment1.ts\n"
            + "#EXTINF:5.005,\n"
            + "#EXT-X-BYTERANGE:100@400\n"
            + "segment2.ts\n"
            + "#EXTINF:5.005,\n"
            + "#EXT-X-BYTERANGE:100@400\n"
            + "segment1.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    List<Segment> segments = playlist.segments;
    @Nullable Segment initializationSegment = segments.get(0).initializationSegment;
    assertThat(initializationSegment.url).isEqualTo("segment1.ts");
    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
    assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
    initializationSegment = segments.get(1).initializationSegment;
    assertThat(initializationSegment.url).isEqualTo("segment2.ts");
    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
    assertThat(initializationSegment.byteRangeLength).isEqualTo(400);
    initializationSegment = segments.get(2).initializationSegment;
    assertThat(initializationSegment.url).isEqualTo("segment1.ts");
    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
    assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
  }

  @Test
  public void encryptedMapTag() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXT-X-KEY:METHOD=AES-128,"
            + "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
            + "#EXT-X-MAP:URI=\"init1.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/32.ts\n"
            + "#EXT-X-KEY:METHOD=NONE\n"
            + "#EXT-X-MAP:URI=\"init2.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/47.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);

    List<Segment> segments = playlist.segments;
    Segment initSegment1 = segments.get(0).initializationSegment;
    assertThat(initSegment1.fullSegmentEncryptionKeyUri)
        .isEqualTo("https://priv.example.com/key.php?r=2680");
    assertThat(initSegment1.encryptionIV).isEqualTo("0x1566B");
    Segment initSegment2 = segments.get(1).initializationSegment;
    assertThat(initSegment2.fullSegmentEncryptionKeyUri).isNull();
    assertThat(initSegment2.encryptionIV).isNull();
  }

  @Test
  public void encryptedMapTagWithNoIvFails() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXT-X-KEY:METHOD=AES-128,"
            + "URI=\"https://priv.example.com/key.php?r=2680\"\n"
            + "#EXT-X-MAP:URI=\"init1.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/32.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));

    try {
      new HlsPlaylistParser().parse(playlistUri, inputStream);
      fail();
    } catch (ParserException e) {
      // Expected because the initialization segment does not have a defined initialization vector,
      // although it is affected by an EXT-X-KEY tag.
    }
  }

  @Test
  public void iframeOnly_withExplicitInitSegment_hasCorrectByteRange() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:6\n"
            + "#EXT-X-MEDIA-SEQUENCE:1616630672\n"
            + "#EXT-X-TARGETDURATION:7\n"
            + "#EXT-X-DISCONTINUITY-SEQUENCE:491 \n"
            + "#EXT-X-MAP:URI=\"iframe0.tsv\",BYTERANGE=\"564@0\"\n"
            + "\n"
            + "#EXT-X-I-FRAMES-ONLY\n"
            + "#EXT-X-PROGRAM-DATE-TIME:2021-04-12T17:08:22.000Z\n"
            + "#EXTINF:1.001000,\n"
            + "#EXT-X-BYTERANGE:121260@1128\n"
            + "iframe0.tsv";

    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist standalonePlaylist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    @Nullable Segment initSegment = standalonePlaylist.segments.get(0).initializationSegment;

    assertThat(standalonePlaylist.segments).hasSize(1);
    assertThat(initSegment.byteRangeLength).isEqualTo(564);
    assertThat(initSegment.byteRangeOffset).isEqualTo(0);
  }

  @Test
  public void multivariantPlaylistAttributeInheritance() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:3\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXTINF:5.005,\n"
            + "02/00/27.ts\n"
            + "#EXT-X-MAP:URI=\"init1.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/32.ts\n"
            + "#EXTINF:5.005,\n"
            + "02/00/42.ts\n"
            + "#EXT-X-MAP:URI=\"init2.ts\""
            + "#EXTINF:5.005,\n"
            + "02/00/47.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist standalonePlaylist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    assertThat(standalonePlaylist.hasIndependentSegments).isFalse();

    inputStream.reset();
    HlsMultivariantPlaylist multivariantPlaylist =
        new HlsMultivariantPlaylist(
            /* baseUri= */ "https://example.com/",
            /* tags= */ Collections.emptyList(),
            /* variants= */ Collections.emptyList(),
            /* videos= */ Collections.emptyList(),
            /* audios= */ Collections.emptyList(),
            /* subtitles= */ Collections.emptyList(),
            /* closedCaptions= */ Collections.emptyList(),
            /* muxedAudioFormat= */ null,
            /* muxedCaptionFormats= */ null,
            /* hasIndependentSegments= */ true,
            /* variableDefinitions= */ Collections.emptyMap(),
            /* sessionKeyDrmInitData= */ Collections.emptyList());
    HlsMediaPlaylist playlistWithInheritance =
        (HlsMediaPlaylist)
            new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
                .parse(playlistUri, inputStream);
    assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
  }

  @Test
  public void variableSubstitution() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/substitution.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:8\n"
            + "#EXT-X-DEFINE:NAME=\"underscore_1\",VALUE=\"{\"\n"
            + "#EXT-X-DEFINE:NAME=\"dash-1\",VALUE=\"replaced_value.ts\"\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXTINF:5.005,\n"
            + "segment1.ts\n"
            + "#EXT-X-MAP:URI=\"{$dash-1}\""
            + "#EXTINF:5.005,\n"
            + "segment{$underscore_1}$name_1}\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
    Segment segment = playlist.segments.get(1);
    assertThat(segment.initializationSegment.url).isEqualTo("replaced_value.ts");
    assertThat(segment.url).isEqualTo("segment{$name_1}");
  }

  @Test
  public void inheritedVariableSubstitution() throws IOException {
    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
    String playlistString =
        "#EXTM3U\n"
            + "#EXT-X-VERSION:8\n"
            + "#EXT-X-TARGETDURATION:5\n"
            + "#EXT-X-MEDIA-SEQUENCE:10\n"
            + "#EXT-X-DEFINE:IMPORT=\"imported_base\"\n"
            + "#EXTINF:5.005,\n"
            + "{$imported_base}1.ts\n"
            + "#EXTINF:5.005,\n"
            + "{$imported_base}2.ts\n"
            + "#EXTINF:5.005,\n"
            + "{$imported_base}3.ts\n"
            + "#EXTINF:5.005,\n"
            + "{$imported_base}4.ts\n";
    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
    HashMap<String, String> variableDefinitions = new HashMap<>();
    variableDefinitions.put("imported_base", "long_path");
    HlsMultivariantPlaylist multivariantPlaylist =
        new HlsMultivariantPlaylist(
            /* baseUri= */ "",
            /* tags= */ Collections.emptyList(),
            /* variants= */ Collections.emptyList(),
            /* videos= */ Collections.emptyList(),
            /* audios= */ Collections.emptyList(),
            /* subtitles= */ Collections.emptyList(),
            /* closedCaptions= */ Collections.emptyList(),
            /* muxedAudioFormat= */ null,
            /* muxedCaptionFormats= */ Collections.emptyList(),
            /* hasIndependentSegments= */ false,
            variableDefinitions,
            /* sessionKeyDrmInitData= */ Collections.emptyList());
    HlsMediaPlaylist playlist =
        (HlsMediaPlaylist)
            new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
                .parse(playlistUri, inputStream);
    for (int i = 1; i <= 4; i++) {
      assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts");
    }
  }
}
