From 6c2d25184c7bc1f8a06adf618345be0102cde9e3 Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 6 Jan 2025 10:43:12 -0800 Subject: [PATCH] Make FragmentedMp4Muxer use OutputStream instead of FileOutputStream. This lets us provide a append-only output stream with overridden write() methods - unlocking use cases where we process the muxed data in a streaming fashion, as it's generated by the fragmented muxer. PiperOrigin-RevId: 712581859 --- .../media3/muxer/FragmentedMp4Muxer.java | 21 ++-- .../media3/muxer/FragmentedMp4Writer.java | 111 ++++++++++++------ 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java index c2f72c0880..a9e489e6fe 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java @@ -28,8 +28,8 @@ import androidx.media3.container.Mp4OrientationData; import androidx.media3.container.Mp4TimestampData; import androidx.media3.container.XmpData; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; /** @@ -87,7 +87,7 @@ public final class FragmentedMp4Muxer implements Muxer { /** A builder for {@link FragmentedMp4Muxer} instances. */ public static final class Builder { - private final FileOutputStream fileOutputStream; + private final OutputStream outputStream; private long fragmentDurationMs; private boolean sampleCopyEnabled; @@ -95,12 +95,11 @@ public final class FragmentedMp4Muxer implements Muxer { /** * Creates a {@link Builder} instance with default values. * - * @param fileOutputStream The {@link FileOutputStream} to write the media data to. This stream - * will be automatically closed by the muxer when {@link FragmentedMp4Muxer#close()} is - * called. + * @param outputStream The {@link OutputStream} to write the media data to. This stream will be + * automatically closed by the muxer when {@link FragmentedMp4Muxer#close()} is called. */ - public Builder(FileOutputStream fileOutputStream) { - this.fileOutputStream = fileOutputStream; + public Builder(OutputStream outputStream) { + this.outputStream = outputStream; fragmentDurationMs = DEFAULT_FRAGMENT_DURATION_MS; sampleCopyEnabled = true; } @@ -137,7 +136,7 @@ public final class FragmentedMp4Muxer implements Muxer { /** Builds a {@link FragmentedMp4Muxer} instance. */ public FragmentedMp4Muxer build() { - return new FragmentedMp4Muxer(fileOutputStream, fragmentDurationMs, sampleCopyEnabled); + return new FragmentedMp4Muxer(outputStream, fragmentDurationMs, sampleCopyEnabled); } } @@ -145,12 +144,12 @@ public final class FragmentedMp4Muxer implements Muxer { private final MetadataCollector metadataCollector; private FragmentedMp4Muxer( - FileOutputStream fileOutputStream, long fragmentDurationMs, boolean sampleCopyEnabled) { - checkNotNull(fileOutputStream); + OutputStream outputStream, long fragmentDurationMs, boolean sampleCopyEnabled) { + checkNotNull(outputStream); metadataCollector = new MetadataCollector(); fragmentedMp4Writer = new FragmentedMp4Writer( - fileOutputStream, + outputStream, metadataCollector, AnnexBToAvccConverter.DEFAULT, fragmentDurationMs, diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java index 4c967ed585..3cef62cd5b 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java @@ -35,10 +35,11 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; import androidx.media3.muxer.Muxer.TrackToken; import com.google.common.collect.ImmutableList; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -63,8 +64,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private final FileOutputStream outputStream; - private final FileChannel output; + /** An {@link OutputStream} that tracks the number of bytes written to the stream. */ + private static class PositionTrackingOutputStream extends OutputStream { + private final OutputStream outputStream; + private long position; + + public PositionTrackingOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.position = 0; + } + + @Override + public void write(int b) throws IOException { + position++; + outputStream.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + position += b.length; + outputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + position += len; + outputStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + + /** Returns the number of bytes written to the stream. */ + public long getPosition() { + return position; + } + } + + private final PositionTrackingOutputStream outputStream; + private final WritableByteChannel outputChannel; private final MetadataCollector metadataCollector; private final AnnexBToAvccConverter annexBToAvccConverter; private final long fragmentDurationUs; @@ -81,7 +126,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Creates an instance. * - * @param outputStream The {@link FileOutputStream} to write the data to. + * @param outputStream The {@link OutputStream} to write the data to. * @param metadataCollector A {@link MetadataCollector}. * @param annexBToAvccConverter The {@link AnnexBToAvccConverter} to be used to convert H.264 and * H.265 NAL units from the Annex-B format (using start codes to delineate NAL units) to the @@ -90,13 +135,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param sampleCopyEnabled Whether sample copying is enabled. */ public FragmentedMp4Writer( - FileOutputStream outputStream, + OutputStream outputStream, MetadataCollector metadataCollector, AnnexBToAvccConverter annexBToAvccConverter, long fragmentDurationMs, boolean sampleCopyEnabled) { - this.outputStream = outputStream; - output = outputStream.getChannel(); + this.outputStream = new PositionTrackingOutputStream(outputStream); + this.outputChannel = Channels.newChannel(this.outputStream); this.metadataCollector = metadataCollector; this.annexBToAvccConverter = annexBToAvccConverter; this.fragmentDurationUs = fragmentDurationMs * 1_000; @@ -144,7 +189,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; try { createFragment(); } finally { - output.close(); + outputChannel.close(); outputStream.close(); } } @@ -200,9 +245,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void createHeader() throws IOException { - output.position(0L); - output.write(Boxes.ftyp()); - output.write( + outputChannel.write(Boxes.ftyp()); + outputChannel.write( Boxes.moov( tracks, metadataCollector, /* isFragmentedMp4= */ true, lastSampleDurationBehavior)); } @@ -241,11 +285,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ ImmutableList trackInfos = processAllTracks(); ImmutableList trafBoxes = - createTrafBoxes(trackInfos, /* moofBoxStartPosition= */ output.position()); + createTrafBoxes(trackInfos, /* moofBoxStartPosition= */ outputStream.getPosition()); if (trafBoxes.isEmpty()) { return; } - output.write(Boxes.moof(Boxes.mfhd(currentFragmentSequenceNumber), trafBoxes)); + outputChannel.write(Boxes.moof(Boxes.mfhd(currentFragmentSequenceNumber), trafBoxes)); writeMdatBox(trackInfos); @@ -253,36 +297,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void writeMdatBox(List trackInfos) throws IOException { - long mdatStartPosition = output.position(); - int mdatHeaderSize = 8; // 4 bytes (box size) + 4 bytes (box name) - ByteBuffer header = ByteBuffer.allocate(mdatHeaderSize); - header.putInt(mdatHeaderSize); // The total box size so far. - header.put(Util.getUtf8Bytes("mdat")); - header.flip(); - output.write(header); - - long bytesWritten = 0; + long totalNumBytesSamples = 0; for (int trackInfoIndex = 0; trackInfoIndex < trackInfos.size(); trackInfoIndex++) { ProcessedTrackInfo currentTrackInfo = trackInfos.get(trackInfoIndex); for (int sampleIndex = 0; sampleIndex < currentTrackInfo.pendingSamplesByteBuffer.size(); sampleIndex++) { - bytesWritten += output.write(currentTrackInfo.pendingSamplesByteBuffer.get(sampleIndex)); + totalNumBytesSamples += + currentTrackInfo.pendingSamplesByteBuffer.get(sampleIndex).remaining(); } } - long currentPosition = output.position(); + int mdatHeaderSize = 8; // 4 bytes (box size) + 4 bytes (box name) + ByteBuffer header = ByteBuffer.allocate(mdatHeaderSize); + long totalMdatSize = mdatHeaderSize + totalNumBytesSamples; - output.position(mdatStartPosition); - ByteBuffer mdatSizeByteBuffer = ByteBuffer.allocate(4); - long mdatSize = bytesWritten + mdatHeaderSize; checkArgument( - mdatSize <= UNSIGNED_INT_MAX_VALUE, + totalMdatSize <= UNSIGNED_INT_MAX_VALUE, "Only 32-bit long mdat size supported in the fragmented MP4"); - mdatSizeByteBuffer.putInt((int) mdatSize); - mdatSizeByteBuffer.flip(); - output.write(mdatSizeByteBuffer); - output.position(currentPosition); + header.putInt((int) totalMdatSize); + header.put(Util.getUtf8Bytes("mdat")); + header.flip(); + outputChannel.write(header); + + for (int trackInfoIndex = 0; trackInfoIndex < trackInfos.size(); trackInfoIndex++) { + ProcessedTrackInfo currentTrackInfo = trackInfos.get(trackInfoIndex); + for (int sampleIndex = 0; + sampleIndex < currentTrackInfo.pendingSamplesByteBuffer.size(); + sampleIndex++) { + outputChannel.write(currentTrackInfo.pendingSamplesByteBuffer.get(sampleIndex)); + } + } } private ImmutableList processAllTracks() {