mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
575a2ebbd8
commit
6c2d25184c
@ -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,
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user