Add support for adding XMP data via transformer

PiperOrigin-RevId: 534801202
This commit is contained in:
sheenachhabra 2023-05-24 13:49:47 +01:00 committed by tonihei
parent 41492b6e29
commit 71facd825e
3 changed files with 128 additions and 20 deletions

View File

@ -0,0 +1,87 @@
/*
* 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.container;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Arrays;
/** Stores XMP data. */
@UnstableApi
public final class XmpData implements Metadata.Entry {
public final byte[] data;
/** Creates an instance. */
public XmpData(byte[] data) {
this.data = data;
}
private XmpData(Parcel in) {
this.data = Util.castNonNull(in.createByteArray());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return Arrays.equals(data, ((XmpData) obj).data);
}
@Override
public int hashCode() {
return Arrays.hashCode(data);
}
@Override
public String toString() {
return "XMP: " + Util.toHexString(data);
}
// Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(data);
}
public static final Parcelable.Creator<XmpData> CREATOR =
new Parcelable.Creator<XmpData>() {
@Override
public XmpData createFromParcel(Parcel in) {
return new XmpData(in);
}
@Override
public XmpData[] newArray(int size) {
return new XmpData[size];
}
};
}

View File

@ -25,6 +25,7 @@ import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.Mp4LocationData;
import androidx.media3.container.XmpData;
import androidx.media3.muxer.Mp4Muxer;
import androidx.media3.muxer.Mp4Muxer.TrackToken;
import com.google.common.collect.ImmutableList;
@ -46,15 +47,16 @@ public final class InAppMuxer implements Muxer {
public interface MetadataProvider {
/**
* Updates the list of {@link Metadata.Entry metadata entries}.
* Updates the list of {@linkplain Metadata.Entry metadata entries}.
*
* <p>A {@link Metadata.Entry} can be added or removed. To modify an existing {@link
* Metadata.Entry}, first remove it and then add a new one.
*
* <p>List of supported {@link Metadata.Entry}:
* <p>List of supported {@linkplain Metadata.Entry metadata entries}:
*
* <ul>
* <li>{@link Mp4LocationData}
* <li>{@link XmpData}
* </ul>
*/
void updateMetadataEntries(Set<Metadata.Entry> metadataEntries);
@ -177,7 +179,7 @@ public final class InAppMuxer implements Muxer {
Metadata.Entry entry = metadata.get(i);
// Keep only supported metadata.
// LINT.IfChange(added_metadata)
if (entry instanceof Mp4LocationData) {
if (entry instanceof Mp4LocationData || entry instanceof XmpData) {
metadataEntries.add(entry);
}
}
@ -185,13 +187,6 @@ public final class InAppMuxer implements Muxer {
@Override
public void release(boolean forCancellation) throws MuxerException {
if (metadataProvider != null) {
Set<Metadata.Entry> metadataEntriesCopy = new LinkedHashSet<>(metadataEntries);
metadataProvider.updateMetadataEntries(metadataEntriesCopy);
metadataEntries.clear();
metadataEntries.addAll(metadataEntriesCopy);
}
writeMetadata();
try {
@ -207,11 +202,22 @@ public final class InAppMuxer implements Muxer {
}
private void writeMetadata() {
if (metadataProvider != null) {
Set<Metadata.Entry> metadataEntriesCopy = new LinkedHashSet<>(metadataEntries);
metadataProvider.updateMetadataEntries(metadataEntriesCopy);
metadataEntries.clear();
metadataEntries.addAll(metadataEntriesCopy);
}
for (Metadata.Entry entry : metadataEntries) {
// LINT.IfChange(written_metadata)
if (entry instanceof Mp4LocationData) {
mp4Muxer.setLocation(
((Mp4LocationData) entry).latitude, ((Mp4LocationData) entry).longitude);
} else if (entry instanceof XmpData) {
mp4Muxer.addXmp(ByteBuffer.wrap(((XmpData) entry).data));
} else {
throw new IllegalStateException("Unsupported Metadata.Entry " + entry.getClass().getName());
}
}
}

View File

@ -16,6 +16,7 @@
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.Uri;
@ -23,13 +24,13 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.Util;
import androidx.media3.container.Mp4LocationData;
import androidx.media3.container.XmpData;
import androidx.media3.extractor.mp4.Mp4Extractor;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.junit.After;
@ -42,6 +43,7 @@ import org.junit.runner.RunWith;
public class TransformerWithInAppMuxerEndToEndTest {
private static final String MP4_FILE_ASSET_DIRECTORY = "asset:///media/";
private static final String H264_MP4 = "mp4/sample.mp4";
private static final String XMP_SAMPLE_DATA = "media/xmp/sample_datetime_xmp.xmp";
private Context context;
private String outputPath;
@ -58,7 +60,6 @@ public class TransformerWithInAppMuxerEndToEndTest {
@Test
public void transmux_withLocationMetadata_outputMatchedExpected() throws Exception {
String outputPath = Util.createTempFile(context, "TransformerTest").getPath();
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory(
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
@ -72,15 +73,8 @@ public class TransformerWithInAppMuxerEndToEndTest {
.setMuxerFactory(inAppMuxerFactory)
.build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + H264_MP4));
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem)
.setEffects(
new Effects(
/* audioProcessors= */ ImmutableList.of(),
/* videoEffects= */ ImmutableList.of()))
.build();
transformer.start(editedMediaItem, outputPath);
transformer.start(mediaItem, outputPath);
TransformerTestRunner.runLooper(transformer);
FakeExtractorOutput fakeExtractorOutput =
@ -92,4 +86,25 @@ public class TransformerWithInAppMuxerEndToEndTest {
fakeExtractorOutput,
TestUtil.getDumpFileName(H264_MP4 + ".with_location_metadata"));
}
@Test
public void transmux_withXmpData_completesSuccessfully() throws Exception {
byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, XMP_SAMPLE_DATA);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory(
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
metadataEntries -> metadataEntries.add(new XmpData(xmpData)));
Transformer transformer =
new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.setMuxerFactory(inAppMuxerFactory)
.build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + H264_MP4));
transformer.start(mediaItem, outputPath);
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
// TODO(b/270956881): Use FakeExtractorOutput once it starts dumping uuid box.
assertThat(exportResult.exportException).isNull();
}
}