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.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,

View File

@ -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<ProcessedTrackInfo> trackInfos = processAllTracks();
ImmutableList<ByteBuffer> 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<ProcessedTrackInfo> 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<ProcessedTrackInfo> processAllTracks() {