diff --git a/libraries/container/src/main/java/androidx/media3/container/XmpData.java b/libraries/container/src/main/java/androidx/media3/container/XmpData.java new file mode 100644 index 0000000000..2322b071df --- /dev/null +++ b/libraries/container/src/main/java/androidx/media3/container/XmpData.java @@ -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 CREATOR = + new Parcelable.Creator() { + + @Override + public XmpData createFromParcel(Parcel in) { + return new XmpData(in); + } + + @Override + public XmpData[] newArray(int size) { + return new XmpData[size]; + } + }; +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java index 731916da36..2fc2fa1e34 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java @@ -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}. * *

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

List of supported {@link Metadata.Entry}: + *

List of supported {@linkplain Metadata.Entry metadata entries}: * *

*/ void updateMetadataEntries(Set 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 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 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()); } } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java index 15c06bf652..59f7c384fe 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java @@ -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(); + } }