Handle muxing with timestamps offset from zero in wrapper

Sources (for example media projection) can populate the `Surface` from
`SurfaceAssetLoader` with timestamps that don't start from zero. But
`MuxerWrapper` assumes the latest sample timestamp can be used as the duration
when it calculates average bitrate and notifies its listener.

This can cause a crash because the calculated average bitrate can be zero if
the denominator duration is large enough.

Use the max minus first sample timestamp across tracks instead to get the
duration.

Side note: the large timestamps from the surface texture when using media
projection arrive unchanged (apart from conversion from ns to us) in effect
implementations and in the muxer wrapper (and are passed to the underlying
muxer). The outputs of media3 muxer and the framework muxer are similar.

PiperOrigin-RevId: 652422674
This commit is contained in:
andrewlewis 2024-07-15 03:27:55 -07:00 committed by Copybara-Service
parent c510ab81bb
commit 4b7cc80593
2 changed files with 85 additions and 3 deletions

View File

@ -135,4 +135,77 @@ public class SurfaceAssetLoaderTest {
assertThat(exportResult.height).isEqualTo(bitmap.getHeight()); assertThat(exportResult.height).isEqualTo(bitmap.getHeight());
assertThat(exportResult.durationMs).isEqualTo(300); assertThat(exportResult.durationMs).isEqualTo(300);
} }
@Test
public void encodingFromSurface_withLargeTimestamps_succeeds() throws Exception {
assumeTrue("ImageWriter with pixel format set requires API 29", Util.SDK_INT >= 29);
SettableFuture<SurfaceAssetLoader> surfaceAssetLoaderSettableFuture = SettableFuture.create();
SettableFuture<Surface> surfaceSettableFuture = SettableFuture.create();
Transformer transformer =
new Transformer.Builder(context)
.setAssetLoaderFactory(
new SurfaceAssetLoader.Factory(
new SurfaceAssetLoader.Callback() {
@Override
public void onSurfaceAssetLoaderCreated(
SurfaceAssetLoader surfaceAssetLoader) {
surfaceAssetLoaderSettableFuture.set(surfaceAssetLoader);
}
@Override
public void onSurfaceReady(Surface surface, EditedMediaItem editedMediaItem) {
surfaceSettableFuture.set(surface);
}
}))
.build();
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(
MediaItem.fromUri(SurfaceAssetLoader.MEDIA_ITEM_URI_SCHEME + ":"))
.build();
ListenableFuture<ExportResult> exportCompletionFuture =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.runAsync(testId, editedMediaItem);
SurfaceAssetLoader surfaceAssetLoader =
surfaceAssetLoaderSettableFuture.get(TIMEOUT_MS, MILLISECONDS);
Bitmap bitmap = BitmapPixelTestUtil.readBitmap(TEST_BITMAP_PATH);
surfaceAssetLoader.setContentFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_RAW)
.setWidth(bitmap.getWidth())
.setHeight(bitmap.getHeight())
.setColorInfo(ColorInfo.SRGB_BT709_FULL)
.build());
Surface surface = surfaceSettableFuture.get(TIMEOUT_MS, MILLISECONDS);
int inputFrameCount = 10;
try (ImageWriter imageWriter =
ImageWriter.newInstance(surface, /* maxImages= */ inputFrameCount, PixelFormat.RGBA_8888)) {
ConditionVariable readyForInputCondition = new ConditionVariable();
imageWriter.setOnImageReleasedListener(
unusedImageWriter -> readyForInputCondition.open(), new Handler(Looper.getMainLooper()));
for (int i = 0; i < inputFrameCount; i++) {
Image image = imageWriter.dequeueInputImage();
// Add a large base offset in nanoseconds.
image.setTimestamp(3_020_642_044_930_642L + i * C.NANOS_PER_SECOND / 30);
BitmapPixelTestUtil.copyRbga8888BitmapToImage(bitmap, image);
readyForInputCondition.close();
imageWriter.queueInputImage(image);
// When frames are queued as fast as possible some can be dropped, so throttle input by
// blocking until the previous frame has been released by the downstream pipeline.
if (i > 0) {
assertThat(readyForInputCondition.block(TIMEOUT_MS)).isTrue();
}
}
}
surfaceAssetLoader.signalEndOfInput();
ExportResult exportResult = exportCompletionFuture.get();
assertThat(exportResult.videoFrameCount).isEqualTo(inputFrameCount);
assertThat(exportResult.width).isEqualTo(bitmap.getWidth());
assertThat(exportResult.height).isEqualTo(bitmap.getHeight());
assertThat(exportResult.durationMs).isEqualTo(300);
}
} }

View File

@ -30,6 +30,7 @@ import static androidx.media3.effect.DebugTraceUtil.EVENT_CAN_WRITE_SAMPLE;
import static androidx.media3.effect.DebugTraceUtil.EVENT_INPUT_ENDED; import static androidx.media3.effect.DebugTraceUtil.EVENT_INPUT_ENDED;
import static androidx.media3.effect.DebugTraceUtil.EVENT_OUTPUT_ENDED; import static androidx.media3.effect.DebugTraceUtil.EVENT_OUTPUT_ENDED;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -161,6 +162,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean isEnded; private boolean isEnded;
private @C.TrackType int previousTrackType; private @C.TrackType int previousTrackType;
private long minTrackTimeUs; private long minTrackTimeUs;
private long minEndedTrackTimeUs;
private long maxEndedTrackTimeUs; private long maxEndedTrackTimeUs;
private @MonotonicNonNull ScheduledFuture<?> abortScheduledFuture; private @MonotonicNonNull ScheduledFuture<?> abortScheduledFuture;
private boolean isAborted; private boolean isAborted;
@ -214,6 +216,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
trackTypeToInfo = new SparseArray<>(); trackTypeToInfo = new SparseArray<>();
previousTrackType = C.TRACK_TYPE_NONE; previousTrackType = C.TRACK_TYPE_NONE;
firstVideoPresentationTimeUs = C.TIME_UNSET; firstVideoPresentationTimeUs = C.TIME_UNSET;
minEndedTrackTimeUs = Long.MAX_VALUE;
abortScheduledExecutorService = Util.newSingleThreadScheduledExecutor(TIMER_THREAD_NAME); abortScheduledExecutorService = Util.newSingleThreadScheduledExecutor(TIMER_THREAD_NAME);
bufferInfo = new BufferInfo(); bufferInfo = new BufferInfo();
} }
@ -562,6 +565,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return false; return false;
} }
if (trackInfo.sampleCount == 0) {
trackInfo.startTimeUs = presentationTimeUs;
}
trackInfo.sampleCount++; trackInfo.sampleCount++;
trackInfo.bytesWritten += data.remaining(); trackInfo.bytesWritten += data.remaining();
trackInfo.timeUs = max(trackInfo.timeUs, presentationTimeUs); trackInfo.timeUs = max(trackInfo.timeUs, presentationTimeUs);
@ -597,6 +603,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
TrackInfo trackInfo = trackTypeToInfo.get(trackType); TrackInfo trackInfo = trackTypeToInfo.get(trackType);
minEndedTrackTimeUs = max(0, min(minEndedTrackTimeUs, trackInfo.startTimeUs));
maxEndedTrackTimeUs = max(maxEndedTrackTimeUs, trackInfo.timeUs); maxEndedTrackTimeUs = max(maxEndedTrackTimeUs, trackInfo.timeUs);
listener.onTrackEnded( listener.onTrackEnded(
trackType, trackInfo.format, trackInfo.getAverageBitrate(), trackInfo.sampleCount); trackType, trackInfo.format, trackInfo.getAverageBitrate(), trackInfo.sampleCount);
@ -621,10 +628,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
long durationMs = usToMs(maxEndedTrackTimeUs - minEndedTrackTimeUs);
if (muxerMode == MUXER_MODE_MUX_PARTIAL if (muxerMode == MUXER_MODE_MUX_PARTIAL
&& muxedPartialVideo && muxedPartialVideo
&& (muxedPartialAudio || trackCount == 1)) { && (muxedPartialAudio || trackCount == 1)) {
listener.onEnded(usToMs(maxEndedTrackTimeUs), getCurrentOutputSizeBytes()); listener.onEnded(durationMs, getCurrentOutputSizeBytes());
if (abortScheduledFuture != null) { if (abortScheduledFuture != null) {
abortScheduledFuture.cancel(/* mayInterruptIfRunning= */ false); abortScheduledFuture.cancel(/* mayInterruptIfRunning= */ false);
} }
@ -632,7 +640,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
if (isEnded) { if (isEnded) {
listener.onEnded(usToMs(maxEndedTrackTimeUs), getCurrentOutputSizeBytes()); listener.onEnded(durationMs, getCurrentOutputSizeBytes());
abortScheduledExecutorService.shutdownNow(); abortScheduledExecutorService.shutdownNow();
} }
} }
@ -775,6 +783,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public final Format format; public final Format format;
public final TrackToken trackToken; public final TrackToken trackToken;
public long startTimeUs;
public long bytesWritten; public long bytesWritten;
public int sampleCount; public int sampleCount;
public long timeUs; public long timeUs;
@ -799,7 +808,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Util.scaleLargeTimestamp( Util.scaleLargeTimestamp(
/* timestamp= */ bytesWritten, /* timestamp= */ bytesWritten,
/* multiplier= */ C.BITS_PER_BYTE * C.MICROS_PER_SECOND, /* multiplier= */ C.BITS_PER_BYTE * C.MICROS_PER_SECOND,
/* divisor= */ timeUs); /* divisor= */ timeUs - startTimeUs);
} }
} }
} }