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
This commit is contained in:
Googler 2025-01-06 10:43:12 -08:00 committed by Copybara-Service
parent 575a2ebbd8
commit 6c2d25184c
2 changed files with 88 additions and 44 deletions

View File

@ -28,8 +28,8 @@ import androidx.media3.container.Mp4OrientationData;
import androidx.media3.container.Mp4TimestampData; import androidx.media3.container.Mp4TimestampData;
import androidx.media3.container.XmpData; import androidx.media3.container.XmpData;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
@ -87,7 +87,7 @@ public final class FragmentedMp4Muxer implements Muxer {
/** A builder for {@link FragmentedMp4Muxer} instances. */ /** A builder for {@link FragmentedMp4Muxer} instances. */
public static final class Builder { public static final class Builder {
private final FileOutputStream fileOutputStream; private final OutputStream outputStream;
private long fragmentDurationMs; private long fragmentDurationMs;
private boolean sampleCopyEnabled; private boolean sampleCopyEnabled;
@ -95,12 +95,11 @@ public final class FragmentedMp4Muxer implements Muxer {
/** /**
* Creates a {@link Builder} instance with default values. * Creates a {@link Builder} instance with default values.
* *
* @param fileOutputStream The {@link FileOutputStream} to write the media data to. This stream * @param outputStream The {@link OutputStream} to write the media data to. This stream will be
* will be automatically closed by the muxer when {@link FragmentedMp4Muxer#close()} is * automatically closed by the muxer when {@link FragmentedMp4Muxer#close()} is called.
* called.
*/ */
public Builder(FileOutputStream fileOutputStream) { public Builder(OutputStream outputStream) {
this.fileOutputStream = fileOutputStream; this.outputStream = outputStream;
fragmentDurationMs = DEFAULT_FRAGMENT_DURATION_MS; fragmentDurationMs = DEFAULT_FRAGMENT_DURATION_MS;
sampleCopyEnabled = true; sampleCopyEnabled = true;
} }
@ -137,7 +136,7 @@ public final class FragmentedMp4Muxer implements Muxer {
/** Builds a {@link FragmentedMp4Muxer} instance. */ /** Builds a {@link FragmentedMp4Muxer} instance. */
public FragmentedMp4Muxer build() { 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 final MetadataCollector metadataCollector;
private FragmentedMp4Muxer( private FragmentedMp4Muxer(
FileOutputStream fileOutputStream, long fragmentDurationMs, boolean sampleCopyEnabled) { OutputStream outputStream, long fragmentDurationMs, boolean sampleCopyEnabled) {
checkNotNull(fileOutputStream); checkNotNull(outputStream);
metadataCollector = new MetadataCollector(); metadataCollector = new MetadataCollector();
fragmentedMp4Writer = fragmentedMp4Writer =
new FragmentedMp4Writer( new FragmentedMp4Writer(
fileOutputStream, outputStream,
metadataCollector, metadataCollector,
AnnexBToAvccConverter.DEFAULT, AnnexBToAvccConverter.DEFAULT,
fragmentDurationMs, fragmentDurationMs,

View File

@ -35,10 +35,11 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.muxer.Muxer.TrackToken; import androidx.media3.muxer.Muxer.TrackToken;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; 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.ArrayList;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -63,8 +64,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private final FileOutputStream outputStream; /** An {@link OutputStream} that tracks the number of bytes written to the stream. */
private final FileChannel output; 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 MetadataCollector metadataCollector;
private final AnnexBToAvccConverter annexBToAvccConverter; private final AnnexBToAvccConverter annexBToAvccConverter;
private final long fragmentDurationUs; private final long fragmentDurationUs;
@ -81,7 +126,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Creates an instance. * 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 metadataCollector A {@link MetadataCollector}.
* @param annexBToAvccConverter The {@link AnnexBToAvccConverter} to be used to convert H.264 and * @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 * 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. * @param sampleCopyEnabled Whether sample copying is enabled.
*/ */
public FragmentedMp4Writer( public FragmentedMp4Writer(
FileOutputStream outputStream, OutputStream outputStream,
MetadataCollector metadataCollector, MetadataCollector metadataCollector,
AnnexBToAvccConverter annexBToAvccConverter, AnnexBToAvccConverter annexBToAvccConverter,
long fragmentDurationMs, long fragmentDurationMs,
boolean sampleCopyEnabled) { boolean sampleCopyEnabled) {
this.outputStream = outputStream; this.outputStream = new PositionTrackingOutputStream(outputStream);
output = outputStream.getChannel(); this.outputChannel = Channels.newChannel(this.outputStream);
this.metadataCollector = metadataCollector; this.metadataCollector = metadataCollector;
this.annexBToAvccConverter = annexBToAvccConverter; this.annexBToAvccConverter = annexBToAvccConverter;
this.fragmentDurationUs = fragmentDurationMs * 1_000; this.fragmentDurationUs = fragmentDurationMs * 1_000;
@ -144,7 +189,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try { try {
createFragment(); createFragment();
} finally { } finally {
output.close(); outputChannel.close();
outputStream.close(); outputStream.close();
} }
} }
@ -200,9 +245,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void createHeader() throws IOException { private void createHeader() throws IOException {
output.position(0L); outputChannel.write(Boxes.ftyp());
output.write(Boxes.ftyp()); outputChannel.write(
output.write(
Boxes.moov( Boxes.moov(
tracks, metadataCollector, /* isFragmentedMp4= */ true, lastSampleDurationBehavior)); tracks, metadataCollector, /* isFragmentedMp4= */ true, lastSampleDurationBehavior));
} }
@ -241,11 +285,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
ImmutableList<ProcessedTrackInfo> trackInfos = processAllTracks(); ImmutableList<ProcessedTrackInfo> trackInfos = processAllTracks();
ImmutableList<ByteBuffer> trafBoxes = ImmutableList<ByteBuffer> trafBoxes =
createTrafBoxes(trackInfos, /* moofBoxStartPosition= */ output.position()); createTrafBoxes(trackInfos, /* moofBoxStartPosition= */ outputStream.getPosition());
if (trafBoxes.isEmpty()) { if (trafBoxes.isEmpty()) {
return; return;
} }
output.write(Boxes.moof(Boxes.mfhd(currentFragmentSequenceNumber), trafBoxes)); outputChannel.write(Boxes.moof(Boxes.mfhd(currentFragmentSequenceNumber), trafBoxes));
writeMdatBox(trackInfos); writeMdatBox(trackInfos);
@ -253,36 +297,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void writeMdatBox(List<ProcessedTrackInfo> trackInfos) throws IOException { private void writeMdatBox(List<ProcessedTrackInfo> trackInfos) throws IOException {
long mdatStartPosition = output.position(); long totalNumBytesSamples = 0;
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;
for (int trackInfoIndex = 0; trackInfoIndex < trackInfos.size(); trackInfoIndex++) { for (int trackInfoIndex = 0; trackInfoIndex < trackInfos.size(); trackInfoIndex++) {
ProcessedTrackInfo currentTrackInfo = trackInfos.get(trackInfoIndex); ProcessedTrackInfo currentTrackInfo = trackInfos.get(trackInfoIndex);
for (int sampleIndex = 0; for (int sampleIndex = 0;
sampleIndex < currentTrackInfo.pendingSamplesByteBuffer.size(); sampleIndex < currentTrackInfo.pendingSamplesByteBuffer.size();
sampleIndex++) { 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( checkArgument(
mdatSize <= UNSIGNED_INT_MAX_VALUE, totalMdatSize <= UNSIGNED_INT_MAX_VALUE,
"Only 32-bit long mdat size supported in the fragmented MP4"); "Only 32-bit long mdat size supported in the fragmented MP4");
mdatSizeByteBuffer.putInt((int) mdatSize); header.putInt((int) totalMdatSize);
mdatSizeByteBuffer.flip(); header.put(Util.getUtf8Bytes("mdat"));
output.write(mdatSizeByteBuffer); header.flip();
output.position(currentPosition); 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<ProcessedTrackInfo> processAllTracks() { private ImmutableList<ProcessedTrackInfo> processAllTracks() {