Open source muxer module
PiperOrigin-RevId: 526683141
This commit is contained in:
parent
324115f6cf
commit
67639cafd7
@ -91,6 +91,9 @@
|
||||
an input frame was pending processing.
|
||||
* Query codecs via `MediaCodecList` instead of using
|
||||
`findDecoder/EncoderForFormat` utilities, to expand support.
|
||||
* Muxer:
|
||||
* Add a new muxer library which can be used to create an MP4 container
|
||||
file.
|
||||
* DASH:
|
||||
* Remove the media time offset from `MediaLoadData.startTimeMs` and
|
||||
`MediaLoadData.endTimeMs` for multi period DASH streams.
|
||||
|
@ -81,6 +81,9 @@ project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/cas
|
||||
include modulePrefix + 'lib-effect'
|
||||
project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/effect')
|
||||
|
||||
include modulePrefix + 'lib-muxer'
|
||||
project(modulePrefix + 'lib-muxer').projectDir = new File(rootDir, 'libraries/muxer')
|
||||
|
||||
include modulePrefix + 'lib-transformer'
|
||||
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')
|
||||
|
||||
|
19
libraries/muxer/README.md
Normal file
19
libraries/muxer/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Muxer module
|
||||
|
||||
Provides functionality for producing media container files.
|
||||
|
||||
## Getting the module
|
||||
|
||||
The easiest way to get the module is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
implementation 'androidx.media3:media3-muxer:1.X.X'
|
||||
```
|
||||
|
||||
where `1.X.X` is the version, which must match the version of the other media
|
||||
modules being used.
|
||||
|
||||
Alternatively, you can clone this GitHub project and depend on the module
|
||||
locally. Instructions for doing this can be found in the [top level README][].
|
||||
|
||||
[top level README]: ../../README.md
|
65
libraries/muxer/build.gradle
Normal file
65
libraries/muxer/build.gradle
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2022 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
testCoverageEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest.assets.srcDir '../test_data/src/test/assets/'
|
||||
test.assets.srcDir '../test_data/src/test/assets/'
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Muxer module'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'lib-common')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'lib-extractor')
|
||||
testImplementation project(modulePrefix + 'test-utils-robolectric')
|
||||
testImplementation project(modulePrefix + 'test-utils')
|
||||
testImplementation project(modulePrefix + 'test-data')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||
androidTestImplementation 'junit:junit:' + junitVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
androidTestImplementation project(modulePrefix + 'test-utils')
|
||||
androidTestImplementation project(modulePrefix + 'lib-extractor')
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifactId = 'media3-muxer'
|
||||
releaseName = 'Media3 Muxer module'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
34
libraries/muxer/src/androidTest/AndroidManifest.xml
Normal file
34
libraries/muxer/src/androidTest/AndroidManifest.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="androidx.media3.muxer">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"
|
||||
android:usesCleartextTraffic="true"/>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="androidx.media3.muxer"
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||
</manifest>
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
/** Utilities for muxer test cases. */
|
||||
/* package */ final class AndroidMuxerTestUtil {
|
||||
private static final String DUMP_FILE_OUTPUT_DIRECTORY = "muxerdumps";
|
||||
private static final String DUMP_FILE_EXTENSION = "dump";
|
||||
|
||||
private AndroidMuxerTestUtil() {}
|
||||
|
||||
public static String getExpectedDumpFilePath(String originalFileName) {
|
||||
return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION;
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaExtractor;
|
||||
import androidx.media3.common.util.MediaFormatUtil;
|
||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||
import androidx.media3.test.utils.DumpFileAsserts;
|
||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/** End to end instrumentation tests for {@link Mp4Muxer}. */
|
||||
@RunWith(Parameterized.class)
|
||||
public class Mp4MuxerEndToEndTest {
|
||||
private static final String H264_MP4 = "sample.mp4";
|
||||
private static final String H265_HDR10_MP4 = "hdr10-720p.mp4";
|
||||
private static final String AV1_MP4 = "sample_av1.mp4";
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static ImmutableList<String> mediaSamples() {
|
||||
return ImmutableList.of(H264_MP4, H265_HDR10_MP4, AV1_MP4);
|
||||
}
|
||||
|
||||
@Parameter public String inputFile;
|
||||
|
||||
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private static final String MP4_FILE_ASSET_DIRECTORY = "media/mp4/";
|
||||
|
||||
private Context context;
|
||||
private String outputPath;
|
||||
private FileOutputStream outputStream;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
outputPath = temporaryFolder.newFile("muxeroutput.mp4").getPath();
|
||||
outputStream = new FileOutputStream(outputPath);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOException {
|
||||
Mp4Muxer mp4Muxer = null;
|
||||
|
||||
try {
|
||||
mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
|
||||
feedInputDataToMuxer(mp4Muxer, inputFile);
|
||||
} finally {
|
||||
if (mp4Muxer != null) {
|
||||
mp4Muxer.close();
|
||||
}
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputPath);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, fakeExtractorOutput, AndroidMuxerTestUtil.getExpectedDumpFilePath(inputFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws IOException {
|
||||
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
|
||||
feedInputDataToMuxer(mp4Muxer, H265_HDR10_MP4);
|
||||
|
||||
// Muxer not closed.
|
||||
|
||||
// Audio sample written = 192 out of 195.
|
||||
// Video sample written = 94 out of 127.
|
||||
// Output is still a valid MP4 file.
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputPath);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
AndroidMuxerTestUtil.getExpectedDumpFilePath("partial_" + H265_HDR10_MP4));
|
||||
}
|
||||
|
||||
private void feedInputDataToMuxer(Mp4Muxer mp4Muxer, String inputFileName) throws IOException {
|
||||
MediaExtractor extractor = new MediaExtractor();
|
||||
extractor.setDataSource(
|
||||
context.getResources().getAssets().openFd(MP4_FILE_ASSET_DIRECTORY + inputFileName));
|
||||
|
||||
List<Mp4Muxer.TrackToken> addedTracks = new ArrayList<>();
|
||||
int sortKey = 0;
|
||||
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||
Mp4Muxer.TrackToken trackToken =
|
||||
mp4Muxer.addTrack(
|
||||
sortKey++, MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i)));
|
||||
addedTracks.add(trackToken);
|
||||
extractor.selectTrack(i);
|
||||
}
|
||||
|
||||
do {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
bufferInfo.flags = extractor.getSampleFlags();
|
||||
bufferInfo.offset = 0;
|
||||
bufferInfo.presentationTimeUs = extractor.getSampleTime();
|
||||
int sampleSize = (int) extractor.getSampleSize();
|
||||
bufferInfo.size = sampleSize;
|
||||
|
||||
ByteBuffer sampleBuffer = ByteBuffer.allocateDirect(sampleSize);
|
||||
extractor.readSampleData(sampleBuffer, /* offset= */ 0);
|
||||
|
||||
sampleBuffer.rewind();
|
||||
|
||||
mp4Muxer.writeSampleData(
|
||||
addedTracks.get(extractor.getSampleTrackIndex()), sampleBuffer, bufferInfo);
|
||||
} while (extractor.advance());
|
||||
|
||||
extractor.release();
|
||||
}
|
||||
}
|
19
libraries/muxer/src/main/AndroidManifest.xml
Normal file
19
libraries/muxer/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="androidx.media3.muxer">
|
||||
<uses-sdk />
|
||||
</manifest>
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Converts a buffer containing H.264/H.265 NAL units from the Annex-B format (ISO/IEC 14496-14
|
||||
* Annex B, which uses start codes to delineate NAL units) to the avcC format (ISO/IEC 14496-15,
|
||||
* which uses length prefixes).
|
||||
*/
|
||||
@UnstableApi
|
||||
public interface AnnexBToAvccConverter {
|
||||
/** Default implementation for {@link AnnexBToAvccConverter}. */
|
||||
AnnexBToAvccConverter DEFAULT =
|
||||
(ByteBuffer inputBuffer) -> {
|
||||
if (!inputBuffer.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkArgument(
|
||||
inputBuffer.position() == 0, "The input buffer should have position set to 0.");
|
||||
|
||||
ImmutableList<ByteBuffer> nalUnitList = AnnexBUtils.findNalUnits(inputBuffer);
|
||||
|
||||
for (int i = 0; i < nalUnitList.size(); i++) {
|
||||
int currentNalUnitLength = nalUnitList.get(i).remaining();
|
||||
|
||||
// Replace the start code with the NAL unit length.
|
||||
inputBuffer.putInt(currentNalUnitLength);
|
||||
|
||||
// Shift the input buffer's position to next start code.
|
||||
int newPosition = inputBuffer.position() + currentNalUnitLength;
|
||||
inputBuffer.position(newPosition);
|
||||
}
|
||||
inputBuffer.rewind();
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes a buffer in-place.
|
||||
*
|
||||
* <p>Expects a {@link ByteBuffer} input with a zero offset.
|
||||
*
|
||||
* @param inputBuffer The buffer to be converted.
|
||||
*/
|
||||
void process(ByteBuffer inputBuffer);
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** NAL unit utilities for start codes and emulation prevention. */
|
||||
/* package */ final class AnnexBUtils {
|
||||
private AnnexBUtils() {}
|
||||
|
||||
/**
|
||||
* Splits a {@link ByteBuffer} into individual NAL units (0x00000001 start code).
|
||||
*
|
||||
* <p>An empty list is returned if the input is not NAL units.
|
||||
*
|
||||
* <p>The position of the input buffer is unchanged after calling this method.
|
||||
*/
|
||||
public static ImmutableList<ByteBuffer> findNalUnits(ByteBuffer input) {
|
||||
if (input.remaining() < 4 || input.getInt(0) != 1) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
ImmutableList.Builder<ByteBuffer> nalUnits = new ImmutableList.Builder<>();
|
||||
|
||||
int lastStart = 4;
|
||||
int zerosSeen = 0;
|
||||
|
||||
for (int i = 4; i < input.limit(); i++) {
|
||||
if (input.get(i) == 1 && zerosSeen >= 3) {
|
||||
// We're just looking at a start code.
|
||||
nalUnits.add(getBytes(input, lastStart, i - 3 - lastStart));
|
||||
lastStart = i + 1;
|
||||
}
|
||||
|
||||
// Handle the end of the stream.
|
||||
if (i == input.limit() - 1) {
|
||||
nalUnits.add(getBytes(input, lastStart, input.limit() - lastStart));
|
||||
}
|
||||
|
||||
if (input.get(i) == 0) {
|
||||
zerosSeen++;
|
||||
} else {
|
||||
zerosSeen = 0;
|
||||
}
|
||||
}
|
||||
input.rewind();
|
||||
return nalUnits.build();
|
||||
}
|
||||
|
||||
/** Removes Annex-B emulation prevention bytes from a buffer. */
|
||||
public static ByteBuffer stripEmulationPrevention(ByteBuffer input) {
|
||||
// For simplicity, we allocate the same number of bytes (although the eventual number might be
|
||||
// smaller).
|
||||
ByteBuffer output = ByteBuffer.allocate(input.limit());
|
||||
int zerosSeen = 0;
|
||||
for (int i = 0; i < input.limit(); i++) {
|
||||
boolean lookingAtEmulationPreventionByte = input.get(i) == 0x03 && zerosSeen >= 2;
|
||||
|
||||
// Only copy bytes if they aren't emulation prevention bytes.
|
||||
if (!lookingAtEmulationPreventionByte) {
|
||||
output.put(input.get(i));
|
||||
}
|
||||
|
||||
if (input.get(i) == 0) {
|
||||
zerosSeen++;
|
||||
} else {
|
||||
zerosSeen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
output.flip();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static ByteBuffer getBytes(ByteBuffer buf, int offset, int length) {
|
||||
ByteBuffer result = buf.duplicate();
|
||||
result.position(offset);
|
||||
result.limit(offset + length);
|
||||
return result.slice();
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/** Utilities for dealing with MP4 boxes. */
|
||||
/* package */ final class BoxUtils {
|
||||
private static final int BOX_TYPE_BYTES = 4;
|
||||
private static final int BOX_SIZE_BYTES = 4;
|
||||
|
||||
private BoxUtils() {}
|
||||
|
||||
/** Wraps content into a box, prefixing it with a length and a box type. */
|
||||
public static ByteBuffer wrapIntoBox(String boxType, ByteBuffer contents) {
|
||||
byte[] typeByteArray = boxType.getBytes(Charsets.UTF_8);
|
||||
return wrapIntoBox(typeByteArray, contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps content into a box, prefixing it with a length and a box type.
|
||||
*
|
||||
* <p>Use this method for box types with special characters. For example location box, which has a
|
||||
* copyright symbol in the beginning.
|
||||
*/
|
||||
public static ByteBuffer wrapIntoBox(byte[] boxType, ByteBuffer contents) {
|
||||
ByteBuffer box = ByteBuffer.allocate(contents.remaining() + BOX_TYPE_BYTES + BOX_SIZE_BYTES);
|
||||
box.putInt(contents.remaining() + BOX_TYPE_BYTES + BOX_SIZE_BYTES);
|
||||
box.put(boxType, 0, BOX_SIZE_BYTES);
|
||||
box.put(contents);
|
||||
box.flip();
|
||||
return box;
|
||||
}
|
||||
|
||||
/** Concatenate multiple boxes into a box, prefixing it with a length and a box type. */
|
||||
public static ByteBuffer wrapBoxesIntoBox(String boxType, List<ByteBuffer> boxes) {
|
||||
int totalSize = BOX_TYPE_BYTES + BOX_SIZE_BYTES;
|
||||
for (int i = 0; i < boxes.size(); i++) {
|
||||
totalSize += boxes.get(i).limit();
|
||||
}
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(totalSize);
|
||||
result.putInt(totalSize);
|
||||
result.put(boxType.getBytes(Charsets.UTF_8), 0, BOX_TYPE_BYTES);
|
||||
|
||||
for (int i = 0; i < boxes.size(); i++) {
|
||||
result.put(boxes.get(i));
|
||||
}
|
||||
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates multiple {@linkplain ByteBuffer byte buffers} into a single {@link ByteBuffer}.
|
||||
*/
|
||||
public static ByteBuffer concatenateBuffers(ByteBuffer... buffers) {
|
||||
int totalSize = 0;
|
||||
for (ByteBuffer buffer : buffers) {
|
||||
totalSize += buffer.limit();
|
||||
}
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(totalSize);
|
||||
for (ByteBuffer buffer : buffers) {
|
||||
result.put(buffer);
|
||||
}
|
||||
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
}
|
1162
libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
Normal file
1162
libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
/** Utilities for color information. */
|
||||
/* package */ final class ColorUtils {
|
||||
// The constants are defined as per ISO/IEC 29199-2 (mentioned in MP4 spec ISO/IEC 14496-12:
|
||||
// 8.5.2.3).
|
||||
|
||||
private static final short TRANSFER_SMPTE170_M = 1; // Main; also 6, 14 and 15
|
||||
private static final short TRANSFER_UNSPECIFIED = 2;
|
||||
private static final short TRANSFER_GAMMA22 = 4;
|
||||
private static final short TRANSFER_GAMMA28 = 5;
|
||||
private static final short TRANSFER_SMPTE240_M = 7;
|
||||
private static final short TRANSFER_LINEAR = 8;
|
||||
private static final short TRANSFER_OTHER = 9; // Also 10
|
||||
private static final short TRANSFER_XV_YCC = 11;
|
||||
private static final short TRANSFER_BT1361 = 12;
|
||||
private static final short TRANSFER_SRGB = 13;
|
||||
private static final short TRANSFER_ST2084 = 16;
|
||||
private static final short TRANSFER_ST428 = 17;
|
||||
private static final short TRANSFER_HLG = 18;
|
||||
|
||||
// MediaFormat contains three color-related fields: "standard", "transfer" and "range". The color
|
||||
// standard maps to "primaries" and "matrix" in the "colr" box, while "transfer" and "range" are
|
||||
// mapped to a single value each (although for "transfer", it's still not the same enum values).
|
||||
private static final short PRIMARIES_BT709_5 = 1;
|
||||
private static final short PRIMARIES_UNSPECIFIED = 2;
|
||||
private static final short PRIMARIES_BT601_6_625 = 5;
|
||||
private static final short PRIMARIES_BT601_6_525 = 6; // It's also 7?
|
||||
private static final short PRIMARIES_GENERIC_FILM = 8;
|
||||
private static final short PRIMARIES_BT2020 = 9;
|
||||
private static final short PRIMARIES_BT470_6_M = 4;
|
||||
|
||||
private static final short MATRIX_UNSPECIFIED = 2;
|
||||
private static final short MATRIX_BT709_5 = 1;
|
||||
private static final short MATRIX_BT601_6 = 6;
|
||||
private static final short MATRIX_SMPTE240_M = 7;
|
||||
private static final short MATRIX_BT2020 = 9;
|
||||
private static final short MATRIX_BT2020_CONSTANT = 10;
|
||||
private static final short MATRIX_BT470_6_M = 4;
|
||||
|
||||
/**
|
||||
* Map from {@link MediaFormat} standards to MP4 primaries and matrix indices.
|
||||
*
|
||||
* <p>The i-th element corresponds to a {@link MediaFormat} value of i.
|
||||
*/
|
||||
public static final ImmutableList<ImmutableList<Short>>
|
||||
MEDIAFORMAT_STANDARD_TO_PRIMARIES_AND_MATRIX =
|
||||
ImmutableList.of(
|
||||
ImmutableList.of(PRIMARIES_UNSPECIFIED, MATRIX_UNSPECIFIED), // Unspecified
|
||||
ImmutableList.of(PRIMARIES_BT709_5, MATRIX_BT709_5), // BT709
|
||||
ImmutableList.of(PRIMARIES_BT601_6_625, MATRIX_BT601_6), // BT601_625
|
||||
ImmutableList.of(PRIMARIES_BT601_6_625, MATRIX_BT709_5), // BT601_625_Unadjusted
|
||||
ImmutableList.of(PRIMARIES_BT601_6_525, MATRIX_BT601_6), // BT601_525
|
||||
ImmutableList.of(PRIMARIES_BT601_6_525, MATRIX_SMPTE240_M), // BT601_525_Unadjusted
|
||||
ImmutableList.of(PRIMARIES_BT2020, MATRIX_BT2020), // BT2020
|
||||
ImmutableList.of(PRIMARIES_BT2020, MATRIX_BT2020_CONSTANT), // BT2020Constant
|
||||
ImmutableList.of(PRIMARIES_BT470_6_M, MATRIX_BT470_6_M), // BT470M
|
||||
ImmutableList.of(PRIMARIES_GENERIC_FILM, MATRIX_BT2020) // Film
|
||||
);
|
||||
|
||||
/**
|
||||
* Map from {@link MediaFormat} standards to MP4 transfer indices.
|
||||
*
|
||||
* <p>The i-th element corresponds to a {@link MediaFormat} value of i.
|
||||
*/
|
||||
public static final ImmutableList<Short> MEDIAFORMAT_TRANSFER_TO_MP4_TRANSFER =
|
||||
ImmutableList.of(
|
||||
TRANSFER_UNSPECIFIED, // Unspecified
|
||||
TRANSFER_LINEAR, // Linear
|
||||
TRANSFER_SRGB, // SRGB
|
||||
TRANSFER_SMPTE170_M, // SMPTE_170M
|
||||
TRANSFER_GAMMA22, // Gamma22
|
||||
TRANSFER_GAMMA28, // Gamma28
|
||||
TRANSFER_ST2084, // ST2084
|
||||
TRANSFER_HLG // HLG
|
||||
);
|
||||
|
||||
private ColorUtils() {}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Collects and provides metadata: location, FPS, XMP data, etc. */
|
||||
/* package */ final class MetadataCollector {
|
||||
public int orientation;
|
||||
public @MonotonicNonNull Mp4Location location;
|
||||
public Map<String, Object> metadataPairs;
|
||||
public long modificationDateUnixMs;
|
||||
public @MonotonicNonNull ByteBuffer xmpData;
|
||||
|
||||
public MetadataCollector() {
|
||||
orientation = 0;
|
||||
metadataPairs = new LinkedHashMap<>();
|
||||
modificationDateUnixMs = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void addXmp(ByteBuffer xmpData) {
|
||||
checkState(this.xmpData == null);
|
||||
this.xmpData = xmpData;
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
public void setLocation(float latitude, float longitude) {
|
||||
location = new Mp4Location(latitude, longitude);
|
||||
}
|
||||
|
||||
public void setCaptureFps(float captureFps) {
|
||||
metadataPairs.put("com.android.capture.fps", captureFps);
|
||||
}
|
||||
|
||||
public void addMetadata(String key, Object value) {
|
||||
metadataPairs.put(key, value);
|
||||
}
|
||||
|
||||
public void setModificationTime(long modificationDateUnixMs) {
|
||||
this.modificationDateUnixMs = modificationDateUnixMs;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
/** Stores location data. */
|
||||
/* package */ final class Mp4Location {
|
||||
public final float latitude;
|
||||
public final float longitude;
|
||||
|
||||
public Mp4Location(float latitude, float longitude) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.muxer.Mp4Utils.MVHD_TIMEBASE;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||
|
||||
/** Builds the moov box structure of an MP4 file. */
|
||||
/* package */ class Mp4MoovStructure {
|
||||
/** Provides track's metadata like media format, written samples. */
|
||||
public interface TrackMetadataProvider {
|
||||
Format format();
|
||||
|
||||
int sortKey();
|
||||
|
||||
int videoUnitTimebase();
|
||||
|
||||
ImmutableList<BufferInfo> writtenSamples();
|
||||
|
||||
ImmutableList<Long> writtenChunkOffsets();
|
||||
|
||||
ImmutableList<Integer> writtenChunkSampleCounts();
|
||||
}
|
||||
|
||||
private final MetadataCollector metadataCollector;
|
||||
private final @Mp4Muxer.LastFrameDurationBehavior int lastFrameDurationBehavior;
|
||||
|
||||
public Mp4MoovStructure(
|
||||
MetadataCollector metadataCollector,
|
||||
@Mp4Muxer.LastFrameDurationBehavior int lastFrameDurationBehavior) {
|
||||
this.metadataCollector = metadataCollector;
|
||||
this.lastFrameDurationBehavior = lastFrameDurationBehavior;
|
||||
}
|
||||
|
||||
/** Generates a mdat header. */
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public ByteBuffer moovMetadataHeader(
|
||||
List<? extends TrackMetadataProvider> tracks, long minInputPtsUs) {
|
||||
List<ByteBuffer> trakBoxes = new ArrayList<>();
|
||||
|
||||
int nextTrackId = 1;
|
||||
long videoDurationUs = 0L;
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
TrackMetadataProvider track = tracks.get(i);
|
||||
if (!track.writtenSamples().isEmpty()) {
|
||||
Format format = track.format();
|
||||
String languageCode = bcp47LanguageTagToIso3(format.language);
|
||||
|
||||
boolean isVideo = MimeTypes.isVideo(format.sampleMimeType);
|
||||
boolean isAudio = MimeTypes.isAudio(format.sampleMimeType);
|
||||
|
||||
// Generate the sample durations to calculate the total duration for tkhd box.
|
||||
List<Long> sampleDurationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
track.writtenSamples(),
|
||||
minInputPtsUs,
|
||||
track.videoUnitTimebase(),
|
||||
lastFrameDurationBehavior);
|
||||
|
||||
long trackDurationInTrackUnitsVu = 0;
|
||||
for (int j = 0; j < sampleDurationsVu.size(); j++) {
|
||||
trackDurationInTrackUnitsVu += sampleDurationsVu.get(j);
|
||||
}
|
||||
|
||||
long trackDurationUs =
|
||||
Mp4Utils.usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase());
|
||||
|
||||
String handlerType = isVideo ? "vide" : (isAudio ? "soun" : "meta");
|
||||
String handlerName = isVideo ? "VideoHandle" : (isAudio ? "SoundHandle" : "MetaHandle");
|
||||
|
||||
ByteBuffer stsd =
|
||||
Boxes.stsd(
|
||||
isVideo
|
||||
? Boxes.videoSampleEntry(format)
|
||||
: (isAudio
|
||||
? Boxes.audioSampleEntry(format)
|
||||
: Boxes.metadataSampleEntry(format)));
|
||||
ByteBuffer stts = Boxes.stts(sampleDurationsVu);
|
||||
ByteBuffer stsz = Boxes.stsz(track.writtenSamples());
|
||||
ByteBuffer stsc = Boxes.stsc(track.writtenChunkSampleCounts());
|
||||
ByteBuffer co64 = Boxes.co64(track.writtenChunkOffsets());
|
||||
|
||||
// The below statement is also a description of how a mdat box looks like, with all the
|
||||
// inner boxes and what they actually store. Although they're technically instance methods,
|
||||
// everything that is written to a box is visible in the argument list.
|
||||
ByteBuffer trakBox =
|
||||
Boxes.trak(
|
||||
Boxes.tkhd(
|
||||
nextTrackId,
|
||||
// Using the time base of the entire file, not that of the track; otherwise,
|
||||
// Quicktime will stretch the audio accordingly, see b/158120042.
|
||||
(int) Mp4Utils.vuFromUs(trackDurationUs, MVHD_TIMEBASE),
|
||||
metadataCollector.modificationDateUnixMs,
|
||||
metadataCollector.orientation,
|
||||
format),
|
||||
Boxes.mdia(
|
||||
Boxes.mdhd(
|
||||
trackDurationInTrackUnitsVu,
|
||||
track.videoUnitTimebase(),
|
||||
metadataCollector.modificationDateUnixMs,
|
||||
languageCode),
|
||||
Boxes.hdlr(handlerType, handlerName),
|
||||
Boxes.minf(
|
||||
isVideo ? Boxes.vmhd() : (isAudio ? Boxes.smhd() : Boxes.nmhd()),
|
||||
Boxes.dinf(Boxes.dref(Boxes.localUrl())),
|
||||
isVideo
|
||||
? Boxes.stbl(
|
||||
stsd, stts, stsz, stsc, co64, Boxes.stss(track.writtenSamples()))
|
||||
: Boxes.stbl(stsd, stts, stsz, stsc, co64))));
|
||||
|
||||
trakBoxes.add(trakBox);
|
||||
videoDurationUs = max(videoDurationUs, trackDurationUs);
|
||||
nextTrackId++;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer mvhdBox =
|
||||
Boxes.mvhd(nextTrackId, metadataCollector.modificationDateUnixMs, videoDurationUs);
|
||||
ByteBuffer udtaBox = Boxes.udta(metadataCollector.location);
|
||||
ByteBuffer metaBox =
|
||||
metadataCollector.metadataPairs.isEmpty()
|
||||
? ByteBuffer.allocate(0)
|
||||
: Boxes.meta(
|
||||
Boxes.hdlr(/* handlerType= */ "mdta", /* handlerName= */ ""),
|
||||
Boxes.keys(Lists.newArrayList(metadataCollector.metadataPairs.keySet())),
|
||||
Boxes.ilst(Lists.newArrayList(metadataCollector.metadataPairs.values())));
|
||||
|
||||
ByteBuffer moovBox;
|
||||
moovBox =
|
||||
Boxes.moov(mvhdBox, udtaBox, metaBox, trakBoxes, /* mvexBox= */ ByteBuffer.allocate(0));
|
||||
|
||||
// Also add XMP if needed
|
||||
if (metadataCollector.xmpData != null) {
|
||||
return BoxUtils.concatenateBuffers(
|
||||
moovBox, Boxes.uuid(Boxes.XMP_UUID, metadataCollector.xmpData.duplicate()));
|
||||
} else {
|
||||
// No need for another copy if there is no XMP to be appended.
|
||||
return moovBox;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns an ISO 639-2/T (ISO3) language code for the IETF BCP 47 language tag. */
|
||||
private static @PolyNull String bcp47LanguageTagToIso3(@PolyNull String languageTag) {
|
||||
if (languageTag == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Locale locale =
|
||||
Util.SDK_INT >= 21 ? Locale.forLanguageTag(languageTag) : new Locale(languageTag);
|
||||
|
||||
return locale.getISO3Language().isEmpty() ? languageTag : locale.getISO3Language();
|
||||
}
|
||||
}
|
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A muxer for creating an MP4 container file.
|
||||
*
|
||||
* <p>The muxer supports writing H264, H265 and AV1 video, AAC audio and metadata.
|
||||
*
|
||||
* <p>All the operations are performed on the caller thread.
|
||||
*
|
||||
* <p>To create an MP4 container file, the caller must:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Add tracks using {@link #addTrack(int, Format)} which will return a {@link TrackToken}.
|
||||
* <li>Use the associated {@link TrackToken} when {@linkplain #writeSampleData(TrackToken,
|
||||
* ByteBuffer, BufferInfo) writing samples} for that track.
|
||||
* <li>{@link #close} the muxer when all data has been written.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Some key points:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Tracks can be added at any point, even after writing some samples to other tracks.
|
||||
* <li>The caller is responsible for ensuring that samples of different track types are well
|
||||
* interleaved by calling {@link #writeSampleData(TrackToken, ByteBuffer, BufferInfo)} in an
|
||||
* order that interleaves samples from different tracks.
|
||||
* <li>When writing a file, if an error occurs and the muxer is not closed, then the output MP4
|
||||
* file may still have some partial data.
|
||||
* </ul>
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class Mp4Muxer {
|
||||
/** A token representing an added track. */
|
||||
public interface TrackToken {}
|
||||
|
||||
/** Behavior for the last sample duration. */
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({
|
||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME
|
||||
})
|
||||
public @interface LastFrameDurationBehavior {}
|
||||
/** Insert a zero-length last sample. */
|
||||
public static final int LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME = 0;
|
||||
/**
|
||||
* Use the difference between the last timestamp and the one before that as the duration of the
|
||||
* last sample.
|
||||
*/
|
||||
public static final int LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION = 1;
|
||||
|
||||
/** A builder for {@link Mp4Muxer} instances. */
|
||||
public static final class Builder {
|
||||
private final FileOutputStream fileOutputStream;
|
||||
private @LastFrameDurationBehavior int lastFrameDurationBehavior;
|
||||
@Nullable private AnnexBToAvccConverter annexBToAvccConverter;
|
||||
|
||||
/**
|
||||
* Creates a {@link Builder} instance with default values.
|
||||
*
|
||||
* @param fileOutputStream The {@link FileOutputStream} to write the media data to.
|
||||
*/
|
||||
public Builder(FileOutputStream fileOutputStream) {
|
||||
this.fileOutputStream = checkNotNull(fileOutputStream);
|
||||
lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link LastFrameDurationBehavior} for the video track.
|
||||
*
|
||||
* <p>The default value is {@link #LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Mp4Muxer.Builder setLastFrameDurationBehavior(
|
||||
@LastFrameDurationBehavior int lastFrameDurationBehavior) {
|
||||
this.lastFrameDurationBehavior = lastFrameDurationBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AnnexBToAvccConverter} to be used by the muxer to convert H.264 and H.265 NAL
|
||||
* units from the Annex-B format (using start codes to delineate NAL units) to the AVCC format
|
||||
* (which uses length prefixes).
|
||||
*
|
||||
* <p>The default value is {@link AnnexBToAvccConverter#DEFAULT}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Mp4Muxer.Builder setAnnexBToAvccConverter(AnnexBToAvccConverter annexBToAvccConverter) {
|
||||
this.annexBToAvccConverter = annexBToAvccConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds an {@link Mp4Muxer} instance. */
|
||||
public Mp4Muxer build() {
|
||||
MetadataCollector metadataCollector = new MetadataCollector();
|
||||
Mp4MoovStructure moovStructure =
|
||||
new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior);
|
||||
Mp4Writer mp4Writer =
|
||||
new Mp4Writer(
|
||||
fileOutputStream,
|
||||
moovStructure,
|
||||
annexBToAvccConverter == null
|
||||
? AnnexBToAvccConverter.DEFAULT
|
||||
: annexBToAvccConverter);
|
||||
|
||||
return new Mp4Muxer(mp4Writer, metadataCollector);
|
||||
}
|
||||
}
|
||||
|
||||
private final Mp4Writer mp4Writer;
|
||||
private final MetadataCollector metadataCollector;
|
||||
|
||||
private Mp4Muxer(Mp4Writer mp4Writer, MetadataCollector metadataCollector) {
|
||||
this.mp4Writer = mp4Writer;
|
||||
this.metadataCollector = metadataCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the orientation hint for the video playback.
|
||||
*
|
||||
* @param orientation The orientation, in degrees.
|
||||
*/
|
||||
public void setOrientation(int orientation) {
|
||||
metadataCollector.setOrientation(orientation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location.
|
||||
*
|
||||
* @param latitude The latitude, in degrees. Its value must be in the range [-90, 90].
|
||||
* @param longitude The longitude, in degrees. Its value must be in the range [-180, 180].
|
||||
*/
|
||||
public void setLocation(
|
||||
@FloatRange(from = -90.0, to = 90.0) float latitude,
|
||||
@FloatRange(from = -180.0, to = 180.0) float longitude) {
|
||||
metadataCollector.setLocation(latitude, longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the capture frame rate.
|
||||
*
|
||||
* @param captureFps The frame rate.
|
||||
*/
|
||||
public void setCaptureFps(float captureFps) {
|
||||
metadataCollector.setCaptureFps(captureFps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file modification time.
|
||||
*
|
||||
* @param modificationDateUnixMs The modification time, in milliseconds since epoch.
|
||||
*/
|
||||
public void setModificationTime(long modificationDateUnixMs) {
|
||||
metadataCollector.setModificationTime(modificationDateUnixMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom metadata.
|
||||
*
|
||||
* @param key The metadata key in {@link String} format.
|
||||
* @param value The metadata value in {@link String} or {@link Float} format.
|
||||
*/
|
||||
public void addMetadata(String key, Object value) {
|
||||
metadataCollector.addMetadata(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds xmp data.
|
||||
*
|
||||
* @param xmp The xmp {@link ByteBuffer}.
|
||||
*/
|
||||
public void addXmp(ByteBuffer xmp) {
|
||||
metadataCollector.addXmp(xmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a track of the given media format.
|
||||
*
|
||||
* <p>Tracks can be added at any point before the muxer is closed, even after writing samples to
|
||||
* other tracks.
|
||||
*
|
||||
* <p>The final order of tracks is determined by the provided sort key. Tracks with a lower sort
|
||||
* key will always have a lower track id than tracks with a higher sort key. Ordering between
|
||||
* tracks with the same sort key is not specified.
|
||||
*
|
||||
* @param sortKey The key used for sorting the track list.
|
||||
* @param format The {@link Format} for the track.
|
||||
* @return A unique {@link TrackToken}. It should be used in {@link #writeSampleData}.
|
||||
*/
|
||||
public TrackToken addTrack(int sortKey, Format format) {
|
||||
return mp4Writer.addTrack(sortKey, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes encoded sample data.
|
||||
*
|
||||
* @param trackToken The {@link TrackToken} for which this sample is being written.
|
||||
* @param byteBuffer The encoded sample.
|
||||
* @param bufferInfo The {@link BufferInfo} related to this sample.
|
||||
* @throws IOException If there is any error while writing data to the disk.
|
||||
*/
|
||||
public void writeSampleData(TrackToken trackToken, ByteBuffer byteBuffer, BufferInfo bufferInfo)
|
||||
throws IOException {
|
||||
mp4Writer.writeSampleData(trackToken, byteBuffer, bufferInfo);
|
||||
}
|
||||
|
||||
/** Closes the MP4 file. */
|
||||
public void close() throws IOException {
|
||||
mp4Writer.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
/** Utilities for MP4 files. */
|
||||
/* package */ final class Mp4Utils {
|
||||
/**
|
||||
* The maximum length of boxes which have fixed sizes.
|
||||
*
|
||||
* <p>Technically, we'd know how long they actually are; this upper bound is much simpler to
|
||||
* produce though and we'll throw if we overflow anyway.
|
||||
*/
|
||||
public static final int MAX_FIXED_LEAF_BOX_SIZE = 200;
|
||||
|
||||
/**
|
||||
* The per-video timebase, used for durations in MVHD and TKHD even if the per-track timebase is
|
||||
* different (e.g. typically the sample rate for audio).
|
||||
*/
|
||||
public static final long MVHD_TIMEBASE = 10_000L;
|
||||
|
||||
private Mp4Utils() {}
|
||||
|
||||
/** Converts microseconds to video units, using the provided timebase. */
|
||||
public static long vuFromUs(long timestampUs, long videoUnitTimebase) {
|
||||
return timestampUs * videoUnitTimebase / 1_000_000L; // (division for us to s conversion)
|
||||
}
|
||||
|
||||
/** Converts video units to microseconds, using the provided timebase. */
|
||||
public static long usFromVu(long timestampVu, long videoUnitTimebase) {
|
||||
return timestampVu * 1_000_000L / videoUnitTimebase;
|
||||
}
|
||||
}
|
@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.muxer.Mp4Muxer.TrackToken;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Range;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** Writes MP4 data to the disk. */
|
||||
/* package */ final class Mp4Writer {
|
||||
private static final long INTERLEAVE_DURATION_US = 1_000_000L;
|
||||
|
||||
private final AtomicBoolean hasWrittenSamples;
|
||||
private final Mp4MoovStructure moovGenerator;
|
||||
private final List<Track> tracks;
|
||||
private final AnnexBToAvccConverter annexBToAvccConverter;
|
||||
private final FileOutputStream outputStream;
|
||||
private final FileChannel output;
|
||||
private long mdatStart;
|
||||
private long mdatEnd;
|
||||
private long mdatDataEnd; // Always <= mdatEnd
|
||||
|
||||
// Typically written from the end of the mdat box to the end of the file.
|
||||
private Range<Long> lastMoovWritten;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param outputStream The {@link FileOutputStream} to write the data to.
|
||||
* @param moovGenerator An {@link Mp4MoovStructure} instance to generate the moov box.
|
||||
* @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
|
||||
* AVCC format (which uses length prefixes).
|
||||
*/
|
||||
public Mp4Writer(
|
||||
FileOutputStream outputStream,
|
||||
Mp4MoovStructure moovGenerator,
|
||||
AnnexBToAvccConverter annexBToAvccConverter) {
|
||||
this.moovGenerator = moovGenerator;
|
||||
this.outputStream = outputStream;
|
||||
this.output = outputStream.getChannel();
|
||||
this.annexBToAvccConverter = annexBToAvccConverter;
|
||||
hasWrittenSamples = new AtomicBoolean(false);
|
||||
tracks = new ArrayList<>();
|
||||
lastMoovWritten = Range.closed(0L, 0L);
|
||||
}
|
||||
|
||||
public TrackToken addTrack(int sortKey, Format format) {
|
||||
Track track = new Track(format, sortKey);
|
||||
tracks.add(track);
|
||||
Collections.sort(tracks, (a, b) -> Integer.compare(a.sortKey, b.sortKey));
|
||||
return track;
|
||||
}
|
||||
|
||||
public void writeSampleData(TrackToken token, ByteBuffer byteBuf, BufferInfo bufferInfo)
|
||||
throws IOException {
|
||||
checkState(token instanceof Track);
|
||||
((Track) token).writeSampleData(byteBuf, bufferInfo);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
flushPending(tracks.get(i));
|
||||
}
|
||||
|
||||
// Leave the file empty if no samples are written.
|
||||
if (hasWrittenSamples.get()) {
|
||||
writeMoovAndTrim();
|
||||
}
|
||||
} finally {
|
||||
output.close();
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeHeader() throws IOException {
|
||||
output.position(0L);
|
||||
output.write(Boxes.ftyp());
|
||||
|
||||
// Start with an empty mdat box.
|
||||
mdatStart = output.position();
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(4 + 4 + 8);
|
||||
header.putInt(1); // 4 bytes, indicating a 64-bit length field
|
||||
header.put(Util.getUtf8Bytes("mdat")); // 4 bytes
|
||||
header.putLong(16); // 8 bytes (the actual length)
|
||||
header.flip();
|
||||
output.write(header);
|
||||
|
||||
// The box includes only its type and length.
|
||||
mdatDataEnd = mdatStart + 16;
|
||||
mdatEnd = mdatDataEnd;
|
||||
}
|
||||
|
||||
private ByteBuffer assembleCurrentMoovData() {
|
||||
long minInputPtsUs = Long.MAX_VALUE;
|
||||
|
||||
// Recalculate the min timestamp every time, in case some new samples have smaller timestamps.
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
Track track = tracks.get(i);
|
||||
if (!track.writtenSamples.isEmpty()) {
|
||||
minInputPtsUs = Math.min(track.writtenSamples.get(0).presentationTimeUs, minInputPtsUs);
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer moovHeader;
|
||||
if (minInputPtsUs != Long.MAX_VALUE) {
|
||||
moovHeader = moovGenerator.moovMetadataHeader(tracks, minInputPtsUs);
|
||||
} else {
|
||||
// Skip moov box, if there are no samples.
|
||||
moovHeader = ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
return moovHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces old moov box with the new one.
|
||||
*
|
||||
* <p>It doesn't really replace the existing moov box, rather it adds a new moov box at the end of
|
||||
* the file. Even if this operation fails, the output MP4 file still has a valid moov box.
|
||||
*
|
||||
* <p>After this operation, the mdat box might have some extra space containing garbage value of
|
||||
* the old moov box. This extra space gets trimmed before closing the file (in {@link
|
||||
* #writeMoovAndTrim()}).
|
||||
*
|
||||
* @param newMoovBoxPosition The new position for the moov box.
|
||||
* @param newMoovBoxData The new moov box data.
|
||||
* @throws IOException If there is any error while writing data to the disk.
|
||||
*/
|
||||
private void safelyReplaceMoov(long newMoovBoxPosition, ByteBuffer newMoovBoxData)
|
||||
throws IOException {
|
||||
checkState(newMoovBoxPosition >= lastMoovWritten.upperEndpoint());
|
||||
checkState(newMoovBoxPosition >= mdatEnd);
|
||||
|
||||
// Write a free box to the end of the file, with the new moov box wrapped into it.
|
||||
output.position(newMoovBoxPosition);
|
||||
output.write(BoxUtils.wrapIntoBox("free", newMoovBoxData.duplicate()));
|
||||
|
||||
// The current state is:
|
||||
// | ftyp | mdat .. .. .. | previous moov | free (new moov)|
|
||||
|
||||
// Increase the length of the mdat box so that it now extends to
|
||||
// the previous moov box and the header of the free box.
|
||||
mdatEnd = newMoovBoxPosition + 8;
|
||||
updateMdatSize();
|
||||
|
||||
lastMoovWritten =
|
||||
Range.closed(newMoovBoxPosition, newMoovBoxPosition + newMoovBoxData.remaining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the final moov box and trims extra space from the mdat box.
|
||||
*
|
||||
* <p>This is done right before closing the file.
|
||||
*
|
||||
* @throws IOException If there is any error while writing data to the disk.
|
||||
*/
|
||||
private void writeMoovAndTrim() throws IOException {
|
||||
// The current state is:
|
||||
// | ftyp | mdat .. .. .. (00 00 00) | moov |
|
||||
|
||||
// To keep the trimming safe, first write the final moov box into the gap at the end of the mdat
|
||||
// box, and only then trim the extra space.
|
||||
ByteBuffer currentMoovData = assembleCurrentMoovData();
|
||||
|
||||
int moovBytesNeeded = currentMoovData.remaining();
|
||||
|
||||
// Write a temporary free box wrapping the new moov box.
|
||||
int moovAndFreeBytesNeeded = moovBytesNeeded + 8;
|
||||
|
||||
if (mdatEnd - mdatDataEnd < moovAndFreeBytesNeeded) {
|
||||
// If the gap is not big enough for the moov box, then extend the mdat box once again. This
|
||||
// involves writing moov box farther away one more time.
|
||||
safelyReplaceMoov(lastMoovWritten.upperEndpoint() + moovAndFreeBytesNeeded, currentMoovData);
|
||||
checkState(mdatEnd - mdatDataEnd >= moovAndFreeBytesNeeded);
|
||||
}
|
||||
|
||||
// Write out the new moov box into the gap.
|
||||
long newMoovLocation = mdatDataEnd;
|
||||
output.position(mdatDataEnd);
|
||||
output.write(currentMoovData);
|
||||
|
||||
// Add a free box to account for the actual remaining length of the file.
|
||||
long remainingLength = lastMoovWritten.upperEndpoint() - (newMoovLocation + moovBytesNeeded);
|
||||
|
||||
// Moov boxes shouldn't be too long; they can fit into a free box with a 32-bit length field.
|
||||
checkState(remainingLength < Integer.MAX_VALUE);
|
||||
|
||||
ByteBuffer freeHeader = ByteBuffer.allocate(4 + 4);
|
||||
freeHeader.putInt((int) remainingLength);
|
||||
freeHeader.put((byte) 'f');
|
||||
freeHeader.put((byte) 'r');
|
||||
freeHeader.put((byte) 'e');
|
||||
freeHeader.put((byte) 'e');
|
||||
freeHeader.flip();
|
||||
output.write(freeHeader);
|
||||
|
||||
// The moov box is actually written inside mdat box so the current state is:
|
||||
// | ftyp | mdat .. .. .. (new moov) (free header ) (00 00 00) | old moov |
|
||||
|
||||
// Now change this to:
|
||||
// | ftyp | mdat .. .. .. | new moov | free (00 00 00) (old moov) |
|
||||
mdatEnd = newMoovLocation;
|
||||
updateMdatSize();
|
||||
lastMoovWritten = Range.closed(newMoovLocation, newMoovLocation + currentMoovData.limit());
|
||||
|
||||
// Remove the free box.
|
||||
output.truncate(newMoovLocation + moovBytesNeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the moov box after accommodating extra bytes needed for the mdat box.
|
||||
*
|
||||
* @param bytesNeeded The extra bytes needed for the mdat box.
|
||||
* @throws IOException If there is any error while writing data to the disk.
|
||||
*/
|
||||
private void rewriteMoovWithMdatEmptySpace(long bytesNeeded) throws IOException {
|
||||
long newMoovStart = Math.max(mdatEnd + bytesNeeded, lastMoovWritten.upperEndpoint());
|
||||
|
||||
ByteBuffer currentMoovData = assembleCurrentMoovData();
|
||||
|
||||
safelyReplaceMoov(newMoovStart, currentMoovData);
|
||||
}
|
||||
|
||||
/** Writes out any pending samples to the file. */
|
||||
private void flushPending(Track track) throws IOException {
|
||||
if (track.pendingSamples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasWrittenSamples.getAndSet(true)) {
|
||||
writeHeader();
|
||||
}
|
||||
|
||||
// Calculate the additional space required.
|
||||
long bytesNeededInMdat = 0L;
|
||||
for (Pair<BufferInfo, ByteBuffer> sample : track.pendingSamples) {
|
||||
bytesNeededInMdat += sample.second.limit();
|
||||
}
|
||||
|
||||
// Drop all zero-length samples.
|
||||
checkState(bytesNeededInMdat > 0);
|
||||
|
||||
// If the required number of bytes doesn't fit in the gap between the actual data and the moov
|
||||
// box, extend the file and write out the moov box to the end again.
|
||||
if (mdatDataEnd + bytesNeededInMdat >= mdatEnd) {
|
||||
// Reserve some extra space than required, so that mdat box extension is less frequent.
|
||||
rewriteMoovWithMdatEmptySpace(
|
||||
/* bytesNeeded= */ getMdatExtensionAmount(mdatDataEnd) + bytesNeededInMdat);
|
||||
}
|
||||
|
||||
track.writtenChunkOffsets.add(mdatDataEnd);
|
||||
track.writtenChunkSampleCounts.add(track.pendingSamples.size());
|
||||
|
||||
do {
|
||||
Pair<BufferInfo, ByteBuffer> pendingPacket = track.pendingSamples.removeFirst();
|
||||
BufferInfo info = pendingPacket.first;
|
||||
ByteBuffer buffer = pendingPacket.second;
|
||||
|
||||
track.writtenSamples.add(info);
|
||||
|
||||
// Convert the H.264/H.265 samples from Annex-B format (output by MediaCodec) to
|
||||
// Avcc format (required by MP4 container).
|
||||
if (MimeTypes.isVideo(track.format.sampleMimeType)) {
|
||||
annexBToAvccConverter.process(buffer);
|
||||
}
|
||||
|
||||
buffer.rewind();
|
||||
|
||||
mdatDataEnd += output.write(buffer, mdatDataEnd);
|
||||
} while (!track.pendingSamples.isEmpty());
|
||||
|
||||
checkState(mdatDataEnd <= mdatEnd);
|
||||
}
|
||||
|
||||
private void updateMdatSize() throws IOException {
|
||||
// Assuming that the mdat box has a 64-bit length, skip the box type (4 bytes) and
|
||||
// the 32-bit box length field (4 bytes).
|
||||
output.position(mdatStart + 8);
|
||||
|
||||
ByteBuffer mdatSize = ByteBuffer.allocate(8); // one long
|
||||
mdatSize.putLong(mdatEnd - mdatStart);
|
||||
mdatSize.flip();
|
||||
output.write(mdatSize);
|
||||
}
|
||||
|
||||
private void doInterleave() throws IOException {
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
Track track = tracks.get(i);
|
||||
// TODO(b/270583563): check if we need to consider the global timestamp instead.
|
||||
if (track.pendingSamples.size() > 2) {
|
||||
BufferInfo firstSampleInfo = checkNotNull(track.pendingSamples.peekFirst()).first;
|
||||
BufferInfo lastSampleInfo = checkNotNull(track.pendingSamples.peekLast()).first;
|
||||
|
||||
if (lastSampleInfo.presentationTimeUs - firstSampleInfo.presentationTimeUs
|
||||
> INTERLEAVE_DURATION_US) {
|
||||
flushPending(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes by which to extend the mdat box.
|
||||
*
|
||||
* @param currentFileLength The length of current file in bytes (except moov box).
|
||||
* @return The mdat box extension amount in bytes.
|
||||
*/
|
||||
private long getMdatExtensionAmount(long currentFileLength) {
|
||||
long minBytesToExtend = 500_000L;
|
||||
float extensionRatio = 0.2f;
|
||||
return max(minBytesToExtend, (long) (extensionRatio * currentFileLength));
|
||||
}
|
||||
|
||||
private class Track implements TrackToken, Mp4MoovStructure.TrackMetadataProvider {
|
||||
private final Format format;
|
||||
private final int sortKey;
|
||||
private final List<BufferInfo> writtenSamples;
|
||||
private final List<Long> writtenChunkOffsets;
|
||||
private final List<Integer> writtenChunkSampleCounts;
|
||||
private final Deque<Pair<BufferInfo, ByteBuffer>> pendingSamples;
|
||||
|
||||
private boolean hadKeyframe = false;
|
||||
|
||||
private Track(Format format, int sortKey) {
|
||||
this.format = format;
|
||||
this.sortKey = sortKey;
|
||||
writtenSamples = new ArrayList<>();
|
||||
writtenChunkOffsets = new ArrayList<>();
|
||||
writtenChunkSampleCounts = new ArrayList<>();
|
||||
pendingSamples = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
public void writeSampleData(ByteBuffer byteBuf, BufferInfo bufferInfo) throws IOException {
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0) {
|
||||
hadKeyframe = true;
|
||||
}
|
||||
|
||||
if (!hadKeyframe && MimeTypes.isVideo(format.sampleMimeType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bufferInfo.size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingSamples.addLast(Pair.create(bufferInfo, byteBuf));
|
||||
doInterleave();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int videoUnitTimebase() {
|
||||
return MimeTypes.isAudio(format.sampleMimeType)
|
||||
? 48_000 // TODO(b/270583563): Update these with actual values from mediaFormat.
|
||||
: 90_000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sortKey() {
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<BufferInfo> writtenSamples() {
|
||||
return ImmutableList.copyOf(writtenSamples);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<Long> writtenChunkOffsets() {
|
||||
return ImmutableList.copyOf(writtenChunkOffsets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<Integer> writtenChunkSampleCounts() {
|
||||
return ImmutableList.copyOf(writtenChunkSampleCounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format format() {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
19
libraries/muxer/src/test/AndroidManifest.xml
Normal file
19
libraries/muxer/src/test/AndroidManifest.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="androidx.media3.muxer.test">
|
||||
<uses-sdk/>
|
||||
</manifest>
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.common.util.Util.getBytesFromHexString;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link AnnexBUtils}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AnnexBUtilsTest {
|
||||
@Test
|
||||
public void findNalUnits_emptyBuffer_returnsEmptyList() {
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString(""));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNalUnits_noNalUnit_returnsEmptyList() {
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("ABCDEFABC"));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNalUnits_singleNalUnit_returnsSingleElement() {
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF"));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).hasSize(1);
|
||||
assertThat(components.get(0)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEF")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNalUnits_multipleNalUnits_allReturned() {
|
||||
ByteBuffer buf =
|
||||
ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF00000001DDCC00000001BBAA"));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).hasSize(3);
|
||||
assertThat(components.get(0)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEF")));
|
||||
assertThat(components.get(1)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("DDCC")));
|
||||
assertThat(components.get(2)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("BBAA")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNalUnits_partialStartCodes_ignored() {
|
||||
// The NAL unit has lots of zeros but no start code.
|
||||
ByteBuffer buf =
|
||||
ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF0000AB0000CDEF00000000AB"));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).hasSize(1);
|
||||
assertThat(components.get(0))
|
||||
.isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEF0000AB0000CDEF00000000AB")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNalUnits_startCodeWithManyZeros_stillSplits() {
|
||||
// The NAL unit has a start code that starts with more than 3 zeros (although too many zeros
|
||||
// aren't allowed).
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF000000000001AB"));
|
||||
|
||||
ImmutableList<ByteBuffer> components = AnnexBUtils.findNalUnits(buf);
|
||||
|
||||
assertThat(components).hasSize(2);
|
||||
assertThat(components.get(0)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEF0000")));
|
||||
assertThat(components.get(1)).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("AB")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripEmulationPrevention_noEmulationPreventionBytes_copiesInput() {
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF000000000001AB"));
|
||||
|
||||
ByteBuffer output = AnnexBUtils.stripEmulationPrevention(buf);
|
||||
|
||||
assertThat(output)
|
||||
.isEqualTo(ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF000000000001AB")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripEmulationPrevention_emulationPreventionPresent_bytesStripped() {
|
||||
// The NAL unit has a 00 00 03 * sequence.
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF00000300000001AB"));
|
||||
|
||||
ByteBuffer output = AnnexBUtils.stripEmulationPrevention(buf);
|
||||
|
||||
assertThat(output)
|
||||
.isEqualTo(ByteBuffer.wrap(getBytesFromHexString("00000001ABCDEF000000000001AB")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripEmulationPrevention_03WithoutEnoughZeros_notStripped() {
|
||||
// The NAL unit has a 03 byte around, but not preceded by enough zeros.
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("ABCDEFABCD0003EFABCD03ABCD"));
|
||||
|
||||
ByteBuffer output = AnnexBUtils.stripEmulationPrevention(buf);
|
||||
|
||||
assertThat(output)
|
||||
.isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEFABCD0003EFABCD03ABCD")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripEmulationPrevention_03AtEnd_stripped() {
|
||||
// The NAL unit has a 03 byte at the very end of the input.
|
||||
ByteBuffer buf = ByteBuffer.wrap(getBytesFromHexString("ABCDEF000003"));
|
||||
|
||||
ByteBuffer output = AnnexBUtils.stripEmulationPrevention(buf);
|
||||
|
||||
assertThat(output).isEqualTo(ByteBuffer.wrap(getBytesFromHexString("ABCDEF0000")));
|
||||
}
|
||||
}
|
@ -0,0 +1,601 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static androidx.media3.muxer.Mp4Muxer.LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION;
|
||||
import static androidx.media3.muxer.Mp4Muxer.LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME;
|
||||
import static androidx.media3.muxer.MuxerTestUtil.getExpectedDumpFilePath;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ColorInfo;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.test.utils.DumpFileAsserts;
|
||||
import androidx.media3.test.utils.DumpableMp4Box;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link Boxes}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BoxesTest {
|
||||
// A typical timescale is ~90_000. We're using 100_000 here to simplify calculations.
|
||||
// This makes one time unit equal to 10 microseconds.
|
||||
private static final int VU_TIMEBASE = 100_000;
|
||||
|
||||
private Context context;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTkhdBox_forVideoTrack_matchesExpected() throws IOException {
|
||||
Format videoFormat = MuxerTestUtil.getFakeVideoFormat();
|
||||
|
||||
ByteBuffer tkhdBox =
|
||||
Boxes.tkhd(
|
||||
/* trackId= */ 1,
|
||||
/* trackDurationVu= */ 5_000_000,
|
||||
/* modificationDateUnixMs= */ 1_000_000_000,
|
||||
/* orientation= */ 90,
|
||||
videoFormat);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(tkhdBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, getExpectedDumpFilePath("video_track_tkhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTkhdBox_forAudioTrack_matchesExpected() throws IOException {
|
||||
Format audioFormat = MuxerTestUtil.getFakeAudioFormat();
|
||||
|
||||
ByteBuffer tkhdBox =
|
||||
Boxes.tkhd(
|
||||
/* trackId= */ 1,
|
||||
/* trackDurationVu= */ 5_000_000,
|
||||
/* modificationDateUnixMs= */ 1_000_000_000,
|
||||
/* orientation= */ 90,
|
||||
audioFormat);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(tkhdBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, getExpectedDumpFilePath("audio_track_tkhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMvhdBox_matchesExpected() throws IOException {
|
||||
ByteBuffer mvhdBox =
|
||||
Boxes.mvhd(
|
||||
/* nextEmptyTrackId= */ 3,
|
||||
/* modificationDateUnixMs= */ 1_000_000_000,
|
||||
/* videoDurationUs= */ 5_000_000);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(mvhdBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("mvhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMdhdBox_matchesExpected() throws IOException {
|
||||
ByteBuffer mdhdBox =
|
||||
Boxes.mdhd(
|
||||
/* trackDurationVu= */ 5_000_000,
|
||||
VU_TIMEBASE,
|
||||
/* modificationDateUnixMs= */ 1_000_000_000,
|
||||
/* languageCode= */ "und");
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(mdhdBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("mdhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVmhdBox_matchesExpected() throws IOException {
|
||||
ByteBuffer vmhdBox = Boxes.vmhd();
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(vmhdBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("vmhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSmhdBox_matchesExpected() throws IOException {
|
||||
ByteBuffer smhdBox = Boxes.smhd();
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(smhdBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("smhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createNmhdBox_matchesExpected() throws IOException {
|
||||
ByteBuffer nmhdBox = Boxes.nmhd();
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(nmhdBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("nmhd_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createEmptyDinfBox_matchesExpected() throws IOException {
|
||||
ByteBuffer dinfBox = Boxes.dinf(Boxes.dref(Boxes.localUrl()));
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(dinfBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("dinf_box_empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHdlrBox_matchesExpected() throws IOException {
|
||||
// Create hdlr box for video track.
|
||||
ByteBuffer hdlrBox = Boxes.hdlr(/* handlerType= */ "vide", /* handlerName= */ "VideoHandle");
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(hdlrBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("hdlr_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUdtaBox_matchesExpected() throws IOException {
|
||||
Mp4Location mp4Location = new Mp4Location(33.0f, -120f);
|
||||
|
||||
ByteBuffer udtaBox = Boxes.udta(mp4Location);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(udtaBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("udta_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createKeysBox_matchesExpected() throws Exception {
|
||||
List<String> keyNames = ImmutableList.of("com.android.version", "com.android.capture.fps");
|
||||
|
||||
ByteBuffer keysBox = Boxes.keys(keyNames);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(keysBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("keys_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createIlstBox_matchesExpected() throws Exception {
|
||||
List<Object> values = ImmutableList.of("11", 120.0f);
|
||||
|
||||
ByteBuffer ilstBox = Boxes.ilst(values);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(ilstBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("ilst_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUuidBox_forXmpData_matchesExpected() throws Exception {
|
||||
ByteBuffer xmpData =
|
||||
ByteBuffer.wrap(TestUtil.getByteArray(context, "media/xmp/sample_datetime_xmp.xmp"));
|
||||
|
||||
ByteBuffer xmpUuidBox = Boxes.uuid(Boxes.XMP_UUID, xmpData);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(xmpUuidBox);
|
||||
DumpFileAsserts.assertOutput(context, dumpableBox, getExpectedDumpFilePath("uuid_box_XMP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createuuidBox_withEmptyXmpData_throws() {
|
||||
ByteBuffer xmpData = ByteBuffer.allocate(0);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Boxes.uuid(Boxes.XMP_UUID, xmpData));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAudioSampleEntryBox_forMp4a_matchesExpected() throws Exception {
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setPeakBitrate(128000)
|
||||
.setSampleRate(48000)
|
||||
.setId(3)
|
||||
.setSampleMimeType("audio/mp4a-latm")
|
||||
.setChannelCount(2)
|
||||
.setAverageBitrate(128000)
|
||||
.setLanguage("```")
|
||||
.setMaxInputSize(502)
|
||||
.setInitializationData(ImmutableList.of(BaseEncoding.base16().decode("1190")))
|
||||
.build();
|
||||
|
||||
ByteBuffer audioSampleEntryBox = Boxes.audioSampleEntry(format);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(audioSampleEntryBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, getExpectedDumpFilePath("audio_sample_entry_box_mp4a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAudioSampleEntryBox_withUnknownAudioFormat_throws() {
|
||||
// The audio format contains an unknown MIME type.
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setPeakBitrate(128000)
|
||||
.setSampleRate(48000)
|
||||
.setId(3)
|
||||
.setSampleMimeType("audio/mp4a-unknown")
|
||||
.setChannelCount(2)
|
||||
.setAverageBitrate(128000)
|
||||
.setLanguage("```")
|
||||
.setMaxInputSize(502)
|
||||
.setInitializationData(ImmutableList.of(BaseEncoding.base16().decode("1190")))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Boxes.audioSampleEntry(format));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVideoSampleEntryBox_forH265_matchesExpected() throws Exception {
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setId(1)
|
||||
.setSampleMimeType("video/hevc")
|
||||
.setWidth(48)
|
||||
.setLanguage("und")
|
||||
.setMaxInputSize(114)
|
||||
.setFrameRate(25)
|
||||
.setHeight(32)
|
||||
.setInitializationData(
|
||||
ImmutableList.of(
|
||||
BaseEncoding.base16()
|
||||
.decode(
|
||||
"0000000140010C01FFFF0408000003009FC800000300001E959809000000014201010408000003009FC800000300001EC1882165959AE4CAE68080000003008000000C84000000014401C173D089")))
|
||||
.build();
|
||||
|
||||
ByteBuffer videoSampleEntryBox = Boxes.videoSampleEntry(format);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(videoSampleEntryBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("video_sample_entry_box_h265"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVideoSampleEntryBox_forH265_hdr10_matchesExpected() throws Exception {
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setPeakBitrate(9200)
|
||||
.setId(1)
|
||||
.setSampleMimeType("video/hevc")
|
||||
.setAverageBitrate(9200)
|
||||
.setLanguage("und")
|
||||
.setWidth(256)
|
||||
.setMaxInputSize(66)
|
||||
.setFrameRate(25)
|
||||
.setHeight(256)
|
||||
.setColorInfo(
|
||||
new ColorInfo.Builder()
|
||||
.setColorSpace(C.COLOR_SPACE_BT2020)
|
||||
.setColorTransfer(C.COLOR_TRANSFER_ST2084)
|
||||
.setColorRange(C.COLOR_RANGE_LIMITED)
|
||||
.build())
|
||||
.setInitializationData(
|
||||
ImmutableList.of(
|
||||
BaseEncoding.base16()
|
||||
.decode(
|
||||
"0000000140010C01FFFF02200000030090000003000003003C9598090000000142010102200000030090000003000003003CA008080404D96566924CAE69C20000030002000003003210000000014401C172B46240")))
|
||||
.build();
|
||||
|
||||
ByteBuffer videoSampleEntryBox = Boxes.videoSampleEntry(format);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(videoSampleEntryBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
dumpableBox,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("video_sample_entry_box_h265_hdr10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVideoSampleEntryBox_forH264_matchesExpected() throws Exception {
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setId(1)
|
||||
.setSampleMimeType("video/avc")
|
||||
.setLanguage("und")
|
||||
.setWidth(10)
|
||||
.setMaxInputSize(39)
|
||||
.setFrameRate(25)
|
||||
.setHeight(12)
|
||||
.setInitializationData(
|
||||
ImmutableList.of(
|
||||
BaseEncoding.base16()
|
||||
.decode("0000000167F4000A919B2BF3CB3640000003004000000C83C4896580"),
|
||||
BaseEncoding.base16().decode("0000000168EBE3C448")))
|
||||
.build();
|
||||
|
||||
ByteBuffer videoSampleEntryBox = Boxes.videoSampleEntry(format);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(videoSampleEntryBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("video_sample_entry_box_h264"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVideoSampleEntryBox_forAv1_matchesExpected() throws IOException {
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setId(1)
|
||||
.setSampleMimeType("video/av01")
|
||||
.setLanguage("und")
|
||||
.setWidth(10)
|
||||
.setMaxInputSize(49)
|
||||
.setFrameRate(25)
|
||||
.setHeight(12)
|
||||
.setInitializationData(
|
||||
ImmutableList.of(BaseEncoding.base16().decode("812000000A09200000019CDBFFF304")))
|
||||
.build();
|
||||
|
||||
ByteBuffer videoSampleEntryBox = Boxes.videoSampleEntry(format);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(videoSampleEntryBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("video_sample_entry_box_av1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createVideoSampleEntryBox_withUnknownVideoFormat_throws() {
|
||||
// The video format contains an unknown MIME type.
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setId(1)
|
||||
.setSampleMimeType("video/someweirdvideoformat")
|
||||
.setWidth(48)
|
||||
.setLanguage("und")
|
||||
.setMaxInputSize(114)
|
||||
.setFrameRate(25)
|
||||
.setHeight(32)
|
||||
.setInitializationData(
|
||||
ImmutableList.of(
|
||||
BaseEncoding.base16()
|
||||
.decode(
|
||||
"0000000140010C01FFFF0408000003009FC800000300001E959809000000014201010408000003009FC800000300001EC1882165959AE4CAE68080000003008000000C84000000014401C173D089")))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Boxes.videoSampleEntry(format));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationShort_returnsSingleZeroLengthSample() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(0L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||
|
||||
assertThat(durationsVu).containsExactly(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationDuplicate_returnsSingleZeroLengthSample() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(0L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
||||
|
||||
assertThat(durationsVu).containsExactly(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_singleSampleAtNonZeroTimestamp_lastFrameDurationShort_returnsSampleLengthEqualsTimestamp() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(5_000L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||
|
||||
assertThat(durationsVu).containsExactly(500L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_singleSampleAtNonZeroTimestamp_lastFrameDurationDuplicate_returnsSampleLengthEqualsTimestamp() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(5_000L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
||||
|
||||
assertThat(durationsVu).containsExactly(500L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_differentSampleDurations_lastFrameDurationShort_returnsLastSampleOfZeroDuration() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||
|
||||
assertThat(durationsVu).containsExactly(3_000L, 5_000L, 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getDurationsVuForStts_differentSampleDurations_lastFrameDurationDuplicate_returnsLastSampleOfDuplicateDuration() {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
||||
|
||||
List<Long> durationsVu =
|
||||
Boxes.durationsVuForStts(
|
||||
sampleBufferInfos,
|
||||
/* minInputPresentationTimestampUs= */ 0L,
|
||||
VU_TIMEBASE,
|
||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
||||
|
||||
assertThat(durationsVu).containsExactly(3_000L, 5_000L, 5_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSttsBox_withSingleSampleDuration_matchesExpected() throws IOException {
|
||||
ImmutableList<Long> sampleDurations = ImmutableList.of(500L);
|
||||
|
||||
ByteBuffer sttsBox = Boxes.stts(sampleDurations);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(sttsBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
dumpableBox,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("stts_box_single_sample_duration"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSttsBox_withAllDifferentSampleDurations_matchesExpected() throws IOException {
|
||||
ImmutableList<Long> sampleDurations = ImmutableList.of(1_000L, 2_000L, 3_000L, 5_000L);
|
||||
|
||||
ByteBuffer sttsBox = Boxes.stts(sampleDurations);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(sttsBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
dumpableBox,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("stts_box_all_different_sample_durations"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSttsBox_withFewConsecutiveSameSampleDurations_matchesExpected()
|
||||
throws IOException {
|
||||
ImmutableList<Long> sampleDurations = ImmutableList.of(1_000L, 2_000L, 2_000L, 2_000L);
|
||||
|
||||
ByteBuffer sttsBox = Boxes.stts(sampleDurations);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(sttsBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
dumpableBox,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("stts_box_few_same_sample_durations"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createStszBox_matchesExpected() throws IOException {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||
createBufferInfoListWithSampleSizes(100, 200, 150, 200);
|
||||
|
||||
ByteBuffer stszBox = Boxes.stsz(sampleBufferInfos);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(stszBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stsz_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createStscBox_matchesExpected() throws IOException {
|
||||
ImmutableList<Integer> chunkSampleCounts = ImmutableList.of(100, 500, 200, 100);
|
||||
|
||||
ByteBuffer stscBox = Boxes.stsc(chunkSampleCounts);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(stscBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stsc_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createCo64Box_matchesExpected() throws IOException {
|
||||
ImmutableList<Long> chunkOffsets = ImmutableList.of(1_000L, 5_000L, 7_000L, 10_000L);
|
||||
|
||||
ByteBuffer co64Box = Boxes.co64(chunkOffsets);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(co64Box);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("co64_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createStssBox_matchesExpected() throws IOException {
|
||||
List<MediaCodec.BufferInfo> sampleBufferInfos = createBufferInfoListWithSomeKeyFrames();
|
||||
|
||||
ByteBuffer stssBox = Boxes.stss(sampleBufferInfos);
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(stssBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stss_box"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFtypBox_matchesExpected() throws IOException {
|
||||
ByteBuffer ftypBox = Boxes.ftyp();
|
||||
|
||||
DumpableMp4Box dumpableBox = new DumpableMp4Box(ftypBox);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("ftyp_box"));
|
||||
}
|
||||
|
||||
private static List<MediaCodec.BufferInfo> createBufferInfoListWithSamplePresentationTimestamps(
|
||||
long... timestampsUs) {
|
||||
List<MediaCodec.BufferInfo> bufferInfoList = new ArrayList<>();
|
||||
for (long timestampUs : timestampsUs) {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
bufferInfo.presentationTimeUs = timestampUs;
|
||||
bufferInfoList.add(bufferInfo);
|
||||
}
|
||||
|
||||
return bufferInfoList;
|
||||
}
|
||||
|
||||
private static List<MediaCodec.BufferInfo> createBufferInfoListWithSampleSizes(int... sizes) {
|
||||
List<MediaCodec.BufferInfo> bufferInfoList = new ArrayList<>();
|
||||
for (int size : sizes) {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
bufferInfo.size = size;
|
||||
bufferInfoList.add(bufferInfo);
|
||||
}
|
||||
|
||||
return bufferInfoList;
|
||||
}
|
||||
|
||||
private static List<MediaCodec.BufferInfo> createBufferInfoListWithSomeKeyFrames() {
|
||||
List<MediaCodec.BufferInfo> bufferInfoList = new ArrayList<>();
|
||||
for (int i = 0; i < 30; i++) {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
if (i % 5 == 0) { // Make every 5th frame as key frame.
|
||||
bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
|
||||
}
|
||||
bufferInfoList.add(bufferInfo);
|
||||
}
|
||||
|
||||
return bufferInfoList;
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link AnnexBToAvccConverter#DEFAULT}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class DefaultAnnexBToAvccConverterTest {
|
||||
@Test
|
||||
public void convertAnnexBToAvcc_singleNalUnit() {
|
||||
ByteBuffer in = generateFakeNalUnitData(1000);
|
||||
// Add start code for the NAL unit.
|
||||
in.put(0, (byte) 0);
|
||||
in.put(1, (byte) 0);
|
||||
in.put(2, (byte) 0);
|
||||
in.put(3, (byte) 1);
|
||||
|
||||
AnnexBToAvccConverter annexBToAvccConverter = AnnexBToAvccConverter.DEFAULT;
|
||||
annexBToAvccConverter.process(in);
|
||||
|
||||
// The start code should get replaced with the length of the NAL unit.
|
||||
assertThat(in.getInt(0)).isEqualTo(996);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertAnnexBToAvcc_twoNalUnits() {
|
||||
ByteBuffer in = generateFakeNalUnitData(1000);
|
||||
// Add start code for the first NAL unit.
|
||||
in.put(0, (byte) 0);
|
||||
in.put(1, (byte) 0);
|
||||
in.put(2, (byte) 0);
|
||||
in.put(3, (byte) 1);
|
||||
|
||||
// Add start code for the second NAL unit.
|
||||
in.put(600, (byte) 0);
|
||||
in.put(601, (byte) 0);
|
||||
in.put(602, (byte) 0);
|
||||
in.put(603, (byte) 1);
|
||||
|
||||
AnnexBToAvccConverter annexBToAvccConverter = AnnexBToAvccConverter.DEFAULT;
|
||||
annexBToAvccConverter.process(in);
|
||||
|
||||
// Both the NAL units should have length headers.
|
||||
assertThat(in.getInt(0)).isEqualTo(596);
|
||||
assertThat(in.getInt(600)).isEqualTo(396);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertAnnexBToAvcc_noNalUnit_outputSameAsInput() {
|
||||
ByteBuffer input = generateFakeNalUnitData(1000);
|
||||
ByteBuffer inputCopy = ByteBuffer.allocate(input.limit());
|
||||
inputCopy.put(input);
|
||||
input.rewind();
|
||||
inputCopy.rewind();
|
||||
|
||||
AnnexBToAvccConverter annexBToAvccConverter = AnnexBToAvccConverter.DEFAULT;
|
||||
annexBToAvccConverter.process(input);
|
||||
|
||||
assertThat(input).isEqualTo(inputCopy);
|
||||
}
|
||||
|
||||
/** Returns {@link ByteBuffer} filled with random NAL unit data without start code. */
|
||||
private static ByteBuffer generateFakeNalUnitData(int length) {
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
// Avoid anything resembling start codes (0x00000001) or emulation prevention byte (0x03).
|
||||
buffer.put((byte) ((i % 250) + 5));
|
||||
}
|
||||
|
||||
buffer.rewind();
|
||||
return buffer;
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||
import androidx.media3.muxer.Mp4Muxer.TrackToken;
|
||||
import androidx.media3.test.utils.DumpFileAsserts;
|
||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** End to end tests for {@link Mp4Muxer}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class Mp4MuxerEndToEndTest {
|
||||
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private Format format;
|
||||
private String outputFilePath;
|
||||
private FileOutputStream outputFileStream;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
outputFilePath = temporaryFolder.newFile("output.mp4").getPath();
|
||||
outputFileStream = new FileOutputStream(outputFilePath);
|
||||
|
||||
format = MuxerTestUtil.getFakeVideoFormat();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMp4File_addTrackAndMetadataButNoSamples_createsEmptyFile() throws IOException {
|
||||
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
mp4Muxer.addTrack(/* sortKey= */ 0, format);
|
||||
mp4Muxer.setOrientation(90);
|
||||
mp4Muxer.addMetadata("key", "value");
|
||||
} finally {
|
||||
mp4Muxer.close();
|
||||
}
|
||||
|
||||
byte[] outputFileBytes = TestUtil.getByteArrayFromFilePath(outputFilePath);
|
||||
assertThat(outputFileBytes).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMp4File_withSameTracksOffset_matchesExpected() throws IOException {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
Pair<ByteBuffer, BufferInfo> track1Sample1 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 100L);
|
||||
Pair<ByteBuffer, BufferInfo> track1Sample2 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 200L);
|
||||
|
||||
Pair<ByteBuffer, BufferInfo> track2Sample1 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 100L);
|
||||
Pair<ByteBuffer, BufferInfo> track2Sample2 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 300L);
|
||||
|
||||
try {
|
||||
TrackToken track1 = mp4Muxer.addTrack(/* sortKey= */ 0, format);
|
||||
mp4Muxer.writeSampleData(track1, track1Sample1.first, track1Sample1.second);
|
||||
mp4Muxer.writeSampleData(track1, track1Sample2.first, track1Sample2.second);
|
||||
|
||||
// Add same track again but with different samples.
|
||||
TrackToken track2 = mp4Muxer.addTrack(/* sortKey= */ 1, format);
|
||||
mp4Muxer.writeSampleData(track2, track2Sample1.first, track2Sample1.second);
|
||||
mp4Muxer.writeSampleData(track2, track2Sample2.first, track2Sample2.second);
|
||||
} finally {
|
||||
mp4Muxer.close();
|
||||
}
|
||||
|
||||
// Presentation timestamps in dump file are:
|
||||
// Track 1 Sample 1 = 0L
|
||||
// Track 1 Sample 2 = 100L
|
||||
// Track 2 Sample 1 = 0L
|
||||
// Track 2 Sample 2 = 200L
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_same_tracks_offset.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMp4File_withDifferentTracksOffset_matchesExpected() throws IOException {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
Pair<ByteBuffer, BufferInfo> track1Sample1 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L);
|
||||
Pair<ByteBuffer, BufferInfo> track1Sample2 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 100L);
|
||||
|
||||
Pair<ByteBuffer, BufferInfo> track2Sample1 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 100L);
|
||||
Pair<ByteBuffer, BufferInfo> track2Sample2 =
|
||||
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 200L);
|
||||
|
||||
try {
|
||||
TrackToken track1 = mp4Muxer.addTrack(/* sortKey= */ 0, format);
|
||||
mp4Muxer.writeSampleData(track1, track1Sample1.first, track1Sample1.second);
|
||||
mp4Muxer.writeSampleData(track1, track1Sample2.first, track1Sample2.second);
|
||||
|
||||
// Add same track again but with different samples.
|
||||
TrackToken track2 = mp4Muxer.addTrack(/* sortKey= */ 1, format);
|
||||
mp4Muxer.writeSampleData(track2, track2Sample1.first, track2Sample1.second);
|
||||
mp4Muxer.writeSampleData(track2, track2Sample2.first, track2Sample2.second);
|
||||
} finally {
|
||||
mp4Muxer.close();
|
||||
}
|
||||
|
||||
// The presentation time of second track's first sample is forcefully changed to 0L.
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_different_tracks_offset.mp4"));
|
||||
}
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||
import androidx.media3.muxer.Mp4Muxer.TrackToken;
|
||||
import androidx.media3.test.utils.DumpFileAsserts;
|
||||
import androidx.media3.test.utils.DumpableMp4Box;
|
||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for metadata written by {@link Mp4Muxer}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class Mp4MuxerMetadataTest {
|
||||
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
// Input files.
|
||||
private static final String XMP_SAMPLE_DATA = "media/xmp/sample_datetime_xmp.xmp";
|
||||
|
||||
private Context context;
|
||||
private String outputFilePath;
|
||||
private FileOutputStream outputFileStream;
|
||||
private Format format;
|
||||
private Pair<ByteBuffer, BufferInfo> sampleAndSampleInfo;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
outputFilePath = temporaryFolder.newFile("output.mp4").getPath();
|
||||
outputFileStream = new FileOutputStream(outputFilePath);
|
||||
|
||||
format = MuxerTestUtil.getFakeVideoFormat();
|
||||
sampleAndSampleInfo = MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_orientationNotSet_setsOrientationTo0() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// No rotationDegrees field in output dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_0_orientation.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_setOrientationTo90_setsOrientationTo90() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
|
||||
muxer.setOrientation(90);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// rotationDegrees = 90 in the output dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_90_orientation.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_setOrientationTo180_setsOrientationTo180() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
|
||||
muxer.setOrientation(180);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// rotationDegrees = 180 in the output dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_180_orientation.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_setOrientationTo270_setsOrientationTo270() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
|
||||
muxer.setOrientation(270);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// rotationDegrees = 270 in the output dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_270_orientation.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_setLocation_setsSameLocation() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
muxer.setLocation(33.0f, -120f);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// Xyz data in track metadata dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_location.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_locationNotSet_setsLocationToNull() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// No xyz data in track metadata dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_null_location.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_setFrameRate_setsSameFrameRate() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
muxer.setCaptureFps(120.0f);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(new Mp4Extractor(), outputFilePath);
|
||||
// android.capture.fps data in the track metadata dump.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
fakeExtractorOutput,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_frame_rate.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_addStringMetadata_matchesExpected() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
muxer.addMetadata("SomeStringKey", "Some Random String");
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
// TODO(b/270956881): Use FakeExtractorOutput once it starts dumping custom metadata from the
|
||||
// meta box.
|
||||
DumpableMp4Box dumpableBox =
|
||||
new DumpableMp4Box(ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(outputFilePath)));
|
||||
// The meta box should be present in the output MP4.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context,
|
||||
dumpableBox,
|
||||
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_string_metadata.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_addFloatMetadata_matchesExpected() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
muxer.addMetadata("SomeStringKey", 10.0f);
|
||||
TrackToken token = muxer.addTrack(/* sortKey= */ 0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
// TODO(b/270956881): Use FakeExtractorOutput once it starts dumping custom metadata from the
|
||||
// meta box.
|
||||
DumpableMp4Box dumpableBox =
|
||||
new DumpableMp4Box(ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(outputFilePath)));
|
||||
// The meta box should be present in the output MP4.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("mp4_with_float_metadata.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeMp4File_addXmp_matchesExpected() throws Exception {
|
||||
Mp4Muxer muxer = new Mp4Muxer.Builder(outputFileStream).build();
|
||||
|
||||
try {
|
||||
muxer.setModificationTime(5000000);
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
byte[] xmpBytes = TestUtil.getByteArray(context, XMP_SAMPLE_DATA);
|
||||
ByteBuffer xmp = ByteBuffer.wrap(xmpBytes);
|
||||
muxer.addXmp(xmp);
|
||||
xmp.rewind();
|
||||
TrackToken token = muxer.addTrack(0, format);
|
||||
muxer.writeSampleData(token, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
|
||||
} finally {
|
||||
muxer.close();
|
||||
}
|
||||
|
||||
// TODO(b/270956881): Use FakeExtractorOutput once it starts dumping uuid box.
|
||||
DumpableMp4Box dumpableBox =
|
||||
new DumpableMp4Box(ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(outputFilePath)));
|
||||
// The uuid box should be present in the output MP4.
|
||||
DumpFileAsserts.assertOutput(
|
||||
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("mp4_with_xmp.mp4"));
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.muxer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.Format;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** Utilities for muxer test cases. */
|
||||
/* package */ class MuxerTestUtil {
|
||||
private static final byte[] FAKE_CSD_0 =
|
||||
BaseEncoding.base16().decode("0000000167F4000A919B2BF3CB3640000003004000000C83C4896580");
|
||||
private static final byte[] FAKE_CSD_1 = BaseEncoding.base16().decode("0000000168EBE3C448");
|
||||
private static final byte[] FAKE_H264_SAMPLE =
|
||||
BaseEncoding.base16()
|
||||
.decode(
|
||||
"0000000167F4000A919B2BF3CB3640000003004000000C83C48965800000000168EBE3C448000001658884002BFFFEF5DBF32CAE4A43FF");
|
||||
|
||||
private static final String DUMP_FILE_OUTPUT_DIRECTORY = "muxerdumps";
|
||||
private static final String DUMP_FILE_EXTENSION = "dump";
|
||||
|
||||
public static String getExpectedDumpFilePath(String originalFileName) {
|
||||
return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
public static Format getFakeVideoFormat() {
|
||||
return new Format.Builder()
|
||||
.setSampleMimeType("video/avc")
|
||||
.setWidth(12)
|
||||
.setHeight(10)
|
||||
.setInitializationData(ImmutableList.of(FAKE_CSD_0, FAKE_CSD_1))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Format getFakeAudioFormat() {
|
||||
return new Format.Builder()
|
||||
.setSampleMimeType("audio/mp4a-latm")
|
||||
.setSampleRate(40000)
|
||||
.setChannelCount(2)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Pair<ByteBuffer, BufferInfo> getFakeSampleAndSampleInfo(long presentationTimeUs) {
|
||||
ByteBuffer sampleDirectBuffer = ByteBuffer.allocateDirect(FAKE_H264_SAMPLE.length);
|
||||
sampleDirectBuffer.put(FAKE_H264_SAMPLE);
|
||||
sampleDirectBuffer.rewind();
|
||||
|
||||
BufferInfo bufferInfo = new BufferInfo();
|
||||
bufferInfo.presentationTimeUs = presentationTimeUs;
|
||||
bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
|
||||
bufferInfo.size = FAKE_H264_SAMPLE.length;
|
||||
|
||||
return new Pair<>(sampleDirectBuffer, bufferInfo);
|
||||
}
|
||||
|
||||
private MuxerTestUtil() {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user