Back to Repositories

Testing HLS Media Playlist Parser Implementation in SmartTube

This test suite validates the parsing functionality of HLS media playlists in the ExoPlayer library, focusing on playlist segment handling, encryption, and variable substitution capabilities.

Test Coverage Overview

The test suite provides comprehensive coverage of HLS media playlist parsing functionality, including:
  • Basic playlist structure and segment information parsing
  • Encryption key handling and DRM integration
  • Gap tag processing
  • Initialization segment mapping
  • Variable substitution mechanisms
Key edge cases around encryption, segment sequencing, and format compatibility are thoroughly tested.

Implementation Analysis

The testing approach uses JUnit with AndroidJUnit4 runner to validate the HlsMediaPlaylistParser implementation. Tests utilize mock playlist data to verify parsing of various HLS features and attributes. The implementation follows a systematic pattern of constructing test playlists, parsing them, and asserting expected behaviors and data structures.

Technical Details

Testing tools and configuration:
  • JUnit test framework with AndroidJUnit4 runner
  • Mock HLS playlist data using ByteArrayInputStream
  • ExoPlayer HLS playlist parser components
  • URI and encryption utilities

Best Practices Demonstrated

The test suite demonstrates several testing best practices:
  • Comprehensive validation of parsing edge cases
  • Systematic test organization by feature area
  • Thorough assertion coverage of parsed data structures
  • Clear test naming and documentation
  • Proper handling of encryption and security aspects

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java

            
/*
 * 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 com.google.android.exoplayer2.source.hls.playlist;

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

import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.util.Util;
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 testParseMediaPlaylist() 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"
            + "#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"
            + "#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();
    List<Segment> segments = mediaPlaylist.segments;
    assertThat(segments).isNotNull();
    assertThat(segments).hasSize(5);

    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(Util.toUpperInvariant(segment.encryptionIV)).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(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo("A7B");
    assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET);
    assertThat(segment.byterangeOffset).isEqualTo(0);
    assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
  }

  @Test
  public void testParseSampleAesMethod() 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 testParseSampleAesCencMethod() 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 testParseSampleAesCtrMethod() 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 testMultipleExtXKeysForSingleSegment() 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 testGapTag() 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"
            + "../dummy.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 testMapTag() 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)
        .isSameAs(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 testEncryptedMapTag() 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 testEncryptedMapTagWithNoIvFails() 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 testMasterPlaylistAttributeInheritance() 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();
    HlsMasterPlaylist masterPlaylist =
        new HlsMasterPlaylist(
            /* 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(masterPlaylist).parse(playlistUri, inputStream);
    assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
  }

  @Test
  public void testVariableSubstitution() 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 testInheritedVariableSubstitution() 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");
    HlsMasterPlaylist masterPlaylist =
        new HlsMasterPlaylist(
            /* 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(masterPlaylist).parse(playlistUri, inputStream);
    for (int i = 1; i <= 4; i++) {
      assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts");
    }
  }
}