diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 26c1a2219b..a55d1ac249 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; @@ -30,6 +31,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; @@ -39,6 +41,9 @@ import java.io.IOException; import java.math.BigInteger; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * An HLS {@link MediaChunk}. @@ -93,7 +98,9 @@ import java.util.concurrent.atomic.AtomicInteger; /* key= */ null); boolean mediaSegmentEncrypted = mediaSegmentKey != null; byte[] mediaSegmentIv = - mediaSegmentEncrypted ? getEncryptionIvArray(mediaSegment.encryptionIV) : null; + mediaSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV)) + : null; DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv); // Init segment. @@ -104,7 +111,9 @@ import java.util.concurrent.atomic.AtomicInteger; if (initSegment != null) { initSegmentEncrypted = initSegmentKey != null; byte[] initSegmentIv = - initSegmentEncrypted ? getEncryptionIvArray(initSegment.encryptionIV) : null; + initSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV)) + : null; Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); initDataSpec = new DataSpec( @@ -170,6 +179,7 @@ import java.util.concurrent.atomic.AtomicInteger; public static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); private static final AtomicInteger uidSource = new AtomicInteger(); @@ -186,8 +196,12 @@ import java.util.concurrent.atomic.AtomicInteger; /** The url of the playlist from which this chunk was obtained. */ public final Uri playlistUrl; - @Nullable private final DataSource initDataSource; - @Nullable private final DataSpec initDataSpec; + // These should be final, but can't be due to + // https://github.com/typetools/checker-framework/issues/2215 + @MonotonicNonNull private DataSource initDataSource; + @MonotonicNonNull private DataSpec initDataSpec; + @MonotonicNonNull private Extractor previousExtractor; + private final boolean isMasterTimestampSource; private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; @@ -195,15 +209,14 @@ import java.util.concurrent.atomic.AtomicInteger; private final HlsExtractorFactory extractorFactory; @Nullable private final List muxedCaptionFormats; @Nullable private final DrmInitData drmInitData; - @Nullable private final Extractor previousExtractor; private final Id3Decoder id3Decoder; private final ParsableByteArray scratchId3Data; private final boolean mediaSegmentEncrypted; private final boolean initSegmentEncrypted; - private Extractor extractor; + @MonotonicNonNull private Extractor extractor; private boolean isExtractorReusable; - private HlsSampleStreamWrapper output; + @MonotonicNonNull private HlsSampleStreamWrapper output; // nextLoadPosition refers to the init segment if initDataLoadRequired is true. // Otherwise, nextLoadPosition refers to the media segment. private int nextLoadPosition; @@ -217,13 +230,13 @@ import java.util.concurrent.atomic.AtomicInteger; DataSpec dataSpec, Format format, boolean mediaSegmentEncrypted, - DataSource initDataSource, + @Nullable DataSource initDataSource, @Nullable DataSpec initDataSpec, boolean initSegmentEncrypted, Uri playlistUrl, @Nullable List muxedCaptionFormats, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkMediaSequence, @@ -247,8 +260,12 @@ import java.util.concurrent.atomic.AtomicInteger; chunkMediaSequence); this.mediaSegmentEncrypted = mediaSegmentEncrypted; this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.initDataSource = initDataSource; - this.initDataSpec = initDataSpec; + // Workaround for https://github.com/typetools/checker-framework/issues/2215 + if (initDataSpec != null) { + this.initDataSpec = initDataSpec; + this.initDataSource = Assertions.checkNotNull(initDataSource); + initDataLoadRequired = true; + } this.initSegmentEncrypted = initSegmentEncrypted; this.playlistUrl = playlistUrl; this.isMasterTimestampSource = isMasterTimestampSource; @@ -257,11 +274,13 @@ import java.util.concurrent.atomic.AtomicInteger; this.extractorFactory = extractorFactory; this.muxedCaptionFormats = muxedCaptionFormats; this.drmInitData = drmInitData; - this.previousExtractor = previousExtractor; + // Workaround for https://github.com/typetools/checker-framework/issues/2215 + if (previousExtractor != null) { + this.previousExtractor = previousExtractor; + } this.id3Decoder = id3Decoder; this.scratchId3Data = scratchId3Data; this.shouldSpliceIn = shouldSpliceIn; - initDataLoadRequired = initDataSpec != null; uid = uidSource.getAndIncrement(); } @@ -289,6 +308,8 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { + // output == null means init() hasn't been called. + Assertions.checkNotNull(output); if (extractor == null && previousExtractor != null) { extractor = previousExtractor; isExtractorReusable = true; @@ -306,15 +327,20 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal methods. + @RequiresNonNull("output") private void maybeLoadInitData() throws IOException, InterruptedException { if (!initDataLoadRequired) { return; } + // initDataLoadRequired => initDataSource != null && initDataSpec != null + Assertions.checkNotNull(initDataSource); + Assertions.checkNotNull(initDataSpec); feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted); nextLoadPosition = 0; initDataLoadRequired = false; } + @RequiresNonNull("output") private void loadMedia() throws IOException, InterruptedException { if (!isMasterTimestampSource) { timestampAdjuster.waitUntilInitialized(); @@ -330,6 +356,7 @@ import java.util.concurrent.atomic.AtomicInteger; * concludes (because of a thrown exception or because the operation finishes), the number of fed * bytes is written to {@code nextLoadPosition}. */ + @RequiresNonNull("output") private void feedDataToExtractor( DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted) throws IOException, InterruptedException { @@ -354,7 +381,7 @@ import java.util.concurrent.atomic.AtomicInteger; try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, /* seekPosition= */ null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); @@ -364,6 +391,8 @@ import java.util.concurrent.atomic.AtomicInteger; } } + @RequiresNonNull("output") + @EnsuresNonNull("extractor") private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) throws IOException, InterruptedException { long bytesToRead = dataSource.open(dataSpec); @@ -483,10 +512,15 @@ import java.util.concurrent.atomic.AtomicInteger; /** * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original * in order to decrypt the loaded data. Else returns the original. + * + *

{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither. */ - private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey, - byte[] encryptionIv) { + private static DataSource buildDataSource( + DataSource dataSource, + @Nullable byte[] fullSegmentEncryptionKey, + @Nullable byte[] encryptionIv) { if (fullSegmentEncryptionKey != null) { + Assertions.checkNotNull(encryptionIv); return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv); } return dataSource;