diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index f4831003c2..8c33796606 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -17,6 +17,13 @@
count constraints as they only apply for playback.
* Track Selection:
* Extractors:
+ * MPEG-TS: Roll forward the change ensuring the last frame is rendered by
+ passing the last access unit of a stream to the sample queue
+ ([#7909](https://github.com/google/ExoPlayer/issues/7909)).
+ Incorporating fixes to resolve the issues that emerged in I-frame only
+ HLS streams([#1150](https://github.com/google/ExoPlayer/issues/1150))
+ and H.262 HLS streams
+ ([#1126](https://github.com/google/ExoPlayer/issues/1126)).
* Audio:
* Video:
* Text:
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/ElementaryStreamReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/ElementaryStreamReader.java
index 6d59a73006..a6decc12a2 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/ElementaryStreamReader.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/ElementaryStreamReader.java
@@ -31,7 +31,7 @@ import androidx.media3.extractor.TrackOutput;
*
{@link #seek()} (optional, to reset the state)
* {@link #packetStarted(long, int)} (to signal the start of a new packet)
* {@link #consume(ParsableByteArray)} (zero or more times, to provide packet data)
- * {@link #packetFinished()} (to signal the end of the current packet)
+ * {@link #packetFinished(boolean)} (to signal the end of the current packet)
* Repeat steps 3-5 for subsequent packets
*
*/
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java
index 8a7fbd609e..3990ae0486 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java
@@ -170,7 +170,6 @@ public final class H264Reader implements ElementaryStreamReader {
public void packetFinished(boolean isEndOfInput) {
assertTracksCreated();
if (isEndOfInput) {
- sampleReader.getSampleIsKeyframe();
sampleReader.end(totalBytesWritten);
}
}
@@ -495,16 +494,24 @@ public final class H264Reader implements ElementaryStreamReader {
sampleIsKeyframe = false;
readingSample = true;
}
- return getSampleIsKeyframe();
+ setSampleIsKeyframe();
+ return sampleIsKeyframe;
}
- public boolean getSampleIsKeyframe() {
+ public void end(long position) {
+ setSampleIsKeyframe();
+ // Output a final sample with the NAL units currently held
+ nalUnitStartPosition = position;
+ outputSample(/* offset= */ 0);
+ readingSample = false;
+ }
+
+ private void setSampleIsKeyframe() {
boolean treatIFrameAsKeyframe =
allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
sampleIsKeyframe |=
nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR
|| (treatIFrameAsKeyframe && nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_NON_IDR);
- return sampleIsKeyframe;
}
private void outputSample(int offset) {
@@ -516,13 +523,6 @@ public final class H264Reader implements ElementaryStreamReader {
output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
}
- public void end(long position) {
- // Output a final sample with the NAL units currently held
- nalUnitStartPosition = position;
- outputSample(/* offset= */ 0);
- readingSample = false;
- }
-
private static final class SliceHeaderData {
private static final int SLICE_TYPE_I = 2;
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java
index c3ca6cacbe..3363ecc012 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java
@@ -175,7 +175,6 @@ public final class H265Reader implements ElementaryStreamReader {
public void packetFinished(boolean isEndOfInput) {
assertTracksCreated();
if (isEndOfInput) {
- sampleReader.getSampleIsKeyframe();
sampleReader.end(totalBytesWritten);
}
}
@@ -378,9 +377,15 @@ public final class H265Reader implements ElementaryStreamReader {
}
}
- public boolean getSampleIsKeyframe() {
+ public void end(long position) {
sampleIsKeyframe = nalUnitHasKeyframeData;
- return sampleIsKeyframe;
+ // Output a sample with the NAL units since the current nalUnitPosition
+ outputSample(/* offset= */ (int) (position - nalUnitPosition));
+ // Output a final sample with the remaining NAL units up to the passed position
+ samplePosition = nalUnitPosition;
+ nalUnitPosition = position;
+ outputSample(/* offset= */ 0);
+ readingSample = false;
}
private void outputSample(int offset) {
@@ -392,16 +397,6 @@ public final class H265Reader implements ElementaryStreamReader {
output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
}
- public void end(long position) {
- // Output a sample with the NAL units since the current nalUnitPosition
- outputSample(/* offset= */ (int) (position - nalUnitPosition));
- // Output a final sample with the remaining NAL units up to the passed position
- samplePosition = nalUnitPosition;
- nalUnitPosition = position;
- outputSample(/* offset= */ 0);
- readingSample = false;
- }
-
/** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */
private static boolean isPrefixNalUnit(int nalUnitType) {
return (VPS_NUT <= nalUnitType && nalUnitType <= AUD_NUT) || nalUnitType == PREFIX_SEI_NUT;
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PesReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PesReader.java
index 3c9862c69b..755672d214 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PesReader.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PesReader.java
@@ -160,11 +160,12 @@ public final class PesReader implements TsPayloadReader {
}
}
- private void setState(int state) {
- this.state = state;
- bytesRead = 0;
- }
-
+ /**
+ * Determines if the parser can consume a dummy end of input indication.
+ *
+ * @param isModeHls {@code True} if operating in HLS (HTTP Live Streaming) mode, {@code false}
+ * otherwise.
+ */
public boolean canConsumeDummyEndOfInput(boolean isModeHls) {
// Pusi only payload to trigger end of sample data is only applicable if
// pes does not have a length field and body is being read, another exclusion
@@ -175,6 +176,11 @@ public final class PesReader implements TsPayloadReader {
&& !(isModeHls && reader instanceof H262Reader);
}
+ private void setState(int state) {
+ this.state = state;
+ bytesRead = 0;
+ }
+
/**
* Continues a read from the provided {@code source} into a given {@code target}. It's assumed
* that the data should be written into {@code target} starting from an offset of zero.
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java
index c29a968be7..160dc81e82 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsExtractor.java
@@ -425,8 +425,9 @@ public final class TsExtractor implements Extractor {
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
long inputLength = input.getLength();
+ boolean isModeHls = mode == MODE_HLS;
if (tracksEnded) {
- boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
+ boolean canReadDuration = inputLength != C.LENGTH_UNSET && !isModeHls;
if (canReadDuration && !durationReader.isDurationReadFinished()) {
return durationReader.readDuration(input, seekPosition, pcrPid);
}
@@ -452,7 +453,6 @@ public final class TsExtractor implements Extractor {
TsPayloadReader payloadReader = tsPayloadReaders.valueAt(i);
if (payloadReader instanceof PesReader) {
PesReader pesReader = (PesReader) payloadReader;
- boolean isModeHls = (mode == MODE_HLS);
if (pesReader.canConsumeDummyEndOfInput(isModeHls)) {
pesReader.consume(new ParsableByteArray(), FLAG_PAYLOAD_UNIT_START_INDICATOR);
}