Skip to content

Commit 0e37bd0

Browse files
committed
Re-define 'max size' of SEI queue to operate on unique timestamps
This ensures it works correctly when there are multiple SEI messages per sample and the max size is set from e.g. H.264's `max_num_reorder_frames`. PiperOrigin-RevId: 694526152 (cherry picked from commit 53953dd)
1 parent dba3110 commit 0e37bd0

File tree

3 files changed

+126
-50
lines changed

3 files changed

+126
-50
lines changed

RELEASENOTES.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release notes
22

3+
### Unreleased changes
4+
5+
* Text:
6+
* Fix garbled CEA-608 subtitles in content with more than one SEI message
7+
per sample.
8+
39
## 1.5
410

511
### 1.5.0-rc01 (2024-11-13)

libraries/container/src/main/java/androidx/media3/container/ReorderingSeiMessageQueue.java

+85-50
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616
package androidx.media3.container;
1717

1818
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19+
import static androidx.media3.common.util.Assertions.checkArgument;
1920
import static androidx.media3.common.util.Assertions.checkState;
2021
import static androidx.media3.common.util.Util.castNonNull;
2122

23+
import androidx.annotation.Nullable;
2224
import androidx.annotation.RestrictTo;
2325
import androidx.media3.common.C;
2426
import androidx.media3.common.util.ParsableByteArray;
2527
import androidx.media3.common.util.UnstableApi;
2628
import java.util.ArrayDeque;
27-
import java.util.Deque;
29+
import java.util.ArrayList;
30+
import java.util.List;
2831
import java.util.PriorityQueue;
29-
import java.util.concurrent.atomic.AtomicLong;
3032

3133
/** A queue of SEI messages, ordered by presentation timestamp. */
3234
@UnstableApi
@@ -40,18 +42,17 @@ public interface SeiConsumer {
4042
}
4143

4244
private final SeiConsumer seiConsumer;
43-
private final AtomicLong tieBreakGenerator = new AtomicLong();
4445

45-
/**
46-
* Pool of re-usable {@link SeiMessage} objects to avoid repeated allocations. Elements should be
47-
* added and removed from the 'tail' of the queue (with {@link Deque#push(Object)} and {@link
48-
* Deque#pop()}), to avoid unnecessary array copying.
49-
*/
50-
private final ArrayDeque<SeiMessage> unusedSeiMessages;
46+
/** Pool of re-usable {@link ParsableByteArray} objects to avoid repeated allocations. */
47+
private final ArrayDeque<ParsableByteArray> unusedParsableByteArrays;
5148

52-
private final PriorityQueue<SeiMessage> pendingSeiMessages;
49+
/** Pool of re-usable {@link SampleSeiMessages} objects to avoid repeated allocations. */
50+
private final ArrayDeque<SampleSeiMessages> unusedSampleSeiMessages;
51+
52+
private final PriorityQueue<SampleSeiMessages> pendingSeiMessages;
5353

5454
private int reorderingQueueSize;
55+
@Nullable private SampleSeiMessages lastQueuedMessage;
5556

5657
/**
5758
* Creates an instance, initially with no max size.
@@ -62,16 +63,24 @@ public interface SeiConsumer {
6263
*/
6364
public ReorderingSeiMessageQueue(SeiConsumer seiConsumer) {
6465
this.seiConsumer = seiConsumer;
65-
unusedSeiMessages = new ArrayDeque<>();
66+
unusedParsableByteArrays = new ArrayDeque<>();
67+
unusedSampleSeiMessages = new ArrayDeque<>();
6668
pendingSeiMessages = new PriorityQueue<>();
6769
reorderingQueueSize = C.LENGTH_UNSET;
6870
}
6971

7072
/**
7173
* Sets the max size of the re-ordering queue.
7274
*
75+
* <p>The size is defined in terms of the number of unique presentation timestamps, rather than
76+
* the number of messages. This ensures that properties like H.264's {@code
77+
* max_number_reorder_frames} can be used to set this max size in the case of multiple SEI
78+
* messages per sample (where multiple SEI messages therefore have the same presentation
79+
* timestamp).
80+
*
7381
* <p>When the queue exceeds this size during a call to {@link #add(long, ParsableByteArray)}, the
74-
* least message is passed to the {@link SeiConsumer} provided during construction.
82+
* messages associated with the least timestamp are passed to the {@link SeiConsumer} provided
83+
* during construction.
7584
*
7685
* <p>If the new size is larger than the number of elements currently in the queue, items are
7786
* removed from the head of the queue (least first) and passed to the {@link SeiConsumer} provided
@@ -86,7 +95,7 @@ public void setMaxSize(int reorderingQueueSize) {
8695
/**
8796
* Returns the maximum size of this queue, or {@link C#LENGTH_UNSET} if it is unbounded.
8897
*
89-
* <p>See {@link #setMaxSize(int)}.
98+
* <p>See {@link #setMaxSize(int)} for details on how size is defined.
9099
*/
91100
public int getMaxSize() {
92101
return reorderingQueueSize;
@@ -95,12 +104,16 @@ public int getMaxSize() {
95104
/**
96105
* Adds a message to the queue.
97106
*
98-
* <p>If this causes the queue to exceed its {@linkplain #setMaxSize(int) max size}, the least
99-
* message (which may be the one passed to this method) is passed to the {@link SeiConsumer}
100-
* provided during construction.
107+
* <p>If this causes the queue to exceed its {@linkplain #setMaxSize(int) max size}, messages
108+
* associated with the least timestamp (which may be the message passed to this method) are passed
109+
* to the {@link SeiConsumer} provided during construction.
110+
*
111+
* <p>Messages with matching timestamps must be added consecutively (this will naturally happen
112+
* when parsing messages from a container).
101113
*
102114
* @param presentationTimeUs The presentation time of the SEI message.
103-
* @param seiBuffer The SEI data. The data will be copied, so the provided object can be re-used.
115+
* @param seiBuffer The SEI data. The data will be copied, so the provided object can be re-used
116+
* after this method returns.
104117
*/
105118
public void add(long presentationTimeUs, ParsableByteArray seiBuffer) {
106119
if (reorderingQueueSize == 0
@@ -110,15 +123,42 @@ && presentationTimeUs < castNonNull(pendingSeiMessages.peek()).presentationTimeU
110123
seiConsumer.consume(presentationTimeUs, seiBuffer);
111124
return;
112125
}
113-
SeiMessage seiMessage =
114-
unusedSeiMessages.isEmpty() ? new SeiMessage() : unusedSeiMessages.poll();
115-
seiMessage.reset(presentationTimeUs, tieBreakGenerator.getAndIncrement(), seiBuffer);
116-
pendingSeiMessages.add(seiMessage);
126+
// Make a local copy of the SEI data so we can store it in the queue and allow the seiBuffer
127+
// parameter to be safely re-used after this add() method returns.
128+
ParsableByteArray seiBufferCopy = copy(seiBuffer);
129+
if (lastQueuedMessage != null && presentationTimeUs == lastQueuedMessage.presentationTimeUs) {
130+
lastQueuedMessage.nalBuffers.add(seiBufferCopy);
131+
return;
132+
}
133+
SampleSeiMessages sampleSeiMessages =
134+
unusedSampleSeiMessages.isEmpty() ? new SampleSeiMessages() : unusedSampleSeiMessages.pop();
135+
sampleSeiMessages.init(presentationTimeUs, seiBufferCopy);
136+
pendingSeiMessages.add(sampleSeiMessages);
137+
lastQueuedMessage = sampleSeiMessages;
117138
if (reorderingQueueSize != C.LENGTH_UNSET) {
118139
flushQueueDownToSize(reorderingQueueSize);
119140
}
120141
}
121142

143+
/**
144+
* Copies {@code input} into a {@link ParsableByteArray} instance from {@link
145+
* #unusedParsableByteArrays}, or a new instance if that is empty.
146+
*/
147+
private ParsableByteArray copy(ParsableByteArray input) {
148+
ParsableByteArray result =
149+
unusedParsableByteArrays.isEmpty()
150+
? new ParsableByteArray()
151+
: unusedParsableByteArrays.pop();
152+
result.reset(input.bytesLeft());
153+
System.arraycopy(
154+
/* src= */ input.getData(),
155+
/* srcPos= */ input.getPosition(),
156+
/* dest= */ result.getData(),
157+
/* destPos= */ 0,
158+
/* length= */ result.bytesLeft());
159+
return result;
160+
}
161+
122162
/**
123163
* Empties the queue, passing all messages (least first) to the {@link SeiConsumer} provided
124164
* during construction.
@@ -129,47 +169,42 @@ public void flush() {
129169

130170
private void flushQueueDownToSize(int targetSize) {
131171
while (pendingSeiMessages.size() > targetSize) {
132-
SeiMessage seiMessage = castNonNull(pendingSeiMessages.poll());
133-
seiConsumer.consume(seiMessage.presentationTimeUs, seiMessage.data);
134-
unusedSeiMessages.push(seiMessage);
172+
SampleSeiMessages sampleSeiMessages = castNonNull(pendingSeiMessages.poll());
173+
for (int i = 0; i < sampleSeiMessages.nalBuffers.size(); i++) {
174+
seiConsumer.consume(
175+
sampleSeiMessages.presentationTimeUs, sampleSeiMessages.nalBuffers.get(i));
176+
unusedParsableByteArrays.push(sampleSeiMessages.nalBuffers.get(i));
177+
}
178+
sampleSeiMessages.nalBuffers.clear();
179+
if (lastQueuedMessage != null
180+
&& lastQueuedMessage.presentationTimeUs == sampleSeiMessages.presentationTimeUs) {
181+
lastQueuedMessage = null;
182+
}
183+
unusedSampleSeiMessages.push(sampleSeiMessages);
135184
}
136185
}
137186

138-
/** Holds data from a SEI sample with its presentation timestamp. */
139-
private static final class SeiMessage implements Comparable<SeiMessage> {
140-
141-
private final ParsableByteArray data;
142-
143-
private long presentationTimeUs;
187+
/** Holds the presentation timestamp of a sample and the data from associated SEI messages. */
188+
private static final class SampleSeiMessages implements Comparable<SampleSeiMessages> {
144189

145-
/**
146-
* {@link PriorityQueue} breaks ties arbitrarily. This field ensures that insertion order is
147-
* preserved when messages have the same {@link #presentationTimeUs}.
148-
*/
149-
private long tieBreak;
190+
public final List<ParsableByteArray> nalBuffers;
191+
public long presentationTimeUs;
150192

151-
public SeiMessage() {
193+
public SampleSeiMessages() {
152194
presentationTimeUs = C.TIME_UNSET;
153-
data = new ParsableByteArray();
195+
nalBuffers = new ArrayList<>();
154196
}
155197

156-
public void reset(long presentationTimeUs, long tieBreak, ParsableByteArray nalBuffer) {
157-
checkState(presentationTimeUs != C.TIME_UNSET);
198+
public void init(long presentationTimeUs, ParsableByteArray nalBuffer) {
199+
checkArgument(presentationTimeUs != C.TIME_UNSET);
200+
checkState(this.nalBuffers.isEmpty());
158201
this.presentationTimeUs = presentationTimeUs;
159-
this.tieBreak = tieBreak;
160-
this.data.reset(nalBuffer.bytesLeft());
161-
System.arraycopy(
162-
/* src= */ nalBuffer.getData(),
163-
/* srcPos= */ nalBuffer.getPosition(),
164-
/* dest= */ data.getData(),
165-
/* destPos= */ 0,
166-
/* length= */ nalBuffer.bytesLeft());
202+
this.nalBuffers.add(nalBuffer);
167203
}
168204

169205
@Override
170-
public int compareTo(SeiMessage other) {
171-
int timeComparison = Long.compare(this.presentationTimeUs, other.presentationTimeUs);
172-
return timeComparison != 0 ? timeComparison : Long.compare(this.tieBreak, other.tieBreak);
206+
public int compareTo(SampleSeiMessages other) {
207+
return Long.compare(this.presentationTimeUs, other.presentationTimeUs);
173208
}
174209
}
175210
}

libraries/container/src/test/java/androidx/media3/container/ReorderingSeiMessageQueueTest.java

+35
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,41 @@ public void withMaxSize_addEmitsWhenQueueIsFull() {
115115
.containsExactly(new SeiMessage(/* presentationTimeUs= */ -123, data2));
116116
}
117117

118+
@Test
119+
public void withMaxSize_addEmitsWhenQueueIsFull_handlesDuplicateTimestamps() {
120+
ArrayList<SeiMessage> emittedMessages = new ArrayList<>();
121+
ReorderingSeiMessageQueue reorderingQueue =
122+
new ReorderingSeiMessageQueue(
123+
(presentationTimeUs, seiBuffer) ->
124+
emittedMessages.add(new SeiMessage(presentationTimeUs, seiBuffer)));
125+
reorderingQueue.setMaxSize(1);
126+
127+
// Deliberately re-use a single ParsableByteArray instance to ensure the implementation is
128+
// copying as required.
129+
ParsableByteArray scratchData = new ParsableByteArray();
130+
byte[] data1 = TestUtil.buildTestData(20);
131+
scratchData.reset(data1);
132+
reorderingQueue.add(/* presentationTimeUs= */ 345, scratchData);
133+
// Add a message with a repeated timestamp which should not trigger the max size.
134+
byte[] data2 = TestUtil.buildTestData(15);
135+
scratchData.reset(data2);
136+
reorderingQueue.add(/* presentationTimeUs= */ 345, scratchData);
137+
byte[] data3 = TestUtil.buildTestData(10);
138+
scratchData.reset(data3);
139+
reorderingQueue.add(/* presentationTimeUs= */ -123, scratchData);
140+
// Add another message to flush out the two t=345 messages.
141+
byte[] data4 = TestUtil.buildTestData(5);
142+
scratchData.reset(data4);
143+
reorderingQueue.add(/* presentationTimeUs= */ 456, scratchData);
144+
145+
assertThat(emittedMessages)
146+
.containsExactly(
147+
new SeiMessage(/* presentationTimeUs= */ -123, data3),
148+
new SeiMessage(/* presentationTimeUs= */ 345, data1),
149+
new SeiMessage(/* presentationTimeUs= */ 345, data2))
150+
.inOrder();
151+
}
152+
118153
/**
119154
* Tests that if a message smaller than all current queue items is added when the queue is full,
120155
* the same {@link ParsableByteArray} instance is passed straight to the output to avoid

0 commit comments

Comments
 (0)