diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.with_location_metadata.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.with_location_metadata.dump new file mode 100644 index 0000000000..3b8734d405 --- /dev/null +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.with_location_metadata.dump @@ -0,0 +1,339 @@ +seekMap: + isSeekable = true + duration = 1065600 + getPosition(0) = [[timeUs=0, position=44]] + getPosition(1) = [[timeUs=0, position=44]] + getPosition(532800) = [[timeUs=0, position=44]] + getPosition(1065600) = [[timeUs=0, position=44]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 32.113037 + metadata = entries=[xyz: latitude=45.0, longitude=-90.0] + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66722 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33355 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133455 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166822 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333655 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266922 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233555 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433755 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367022 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567222 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467122 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533855 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633955 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667322 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834155 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767422 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734055 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967622 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867522 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934255 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + peakBitrate = 200000 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[xyz: latitude=45.0, longitude=-90.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 0 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 67208 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 90437 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 113645 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 136875 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 160083 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 183312 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 206520 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 229750 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 252958 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 276187 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 299416 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 322625 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 345854 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 369062 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 392291 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 415500 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 438729 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 461958 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 485166 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 508395 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 531604 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 554833 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 578041 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 601270 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 624479 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 647708 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 670937 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 694145 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 717375 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 740583 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 763812 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 787020 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 810250 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 833458 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 856687 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 879916 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 903125 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 926354 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 949562 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 972791 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 996000 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1019229 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1042437 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1065666 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true 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 b44490d7f7..731916da36 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java @@ -36,27 +36,60 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; /** {@link Muxer} implementation that uses a {@link Mp4Muxer}. */ @UnstableApi public final class InAppMuxer implements Muxer { + + /** Provides {@linkplain Metadata.Entry metadata} to add in the output MP4 file. */ + public interface MetadataProvider { + + /** + * Updates the list of {@link 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}: + * + *

+ */ + void updateMetadataEntries(Set metadataEntries); + } + /** {@link Muxer.Factory} for {@link InAppMuxer}. */ public static final class Factory implements Muxer.Factory { private final long maxDelayBetweenSamplesMs; + private final @Nullable MetadataProvider metadataProvider; /** * Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs} - * set to {@link DefaultMuxer.Factory#DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS}. + * set to {@link DefaultMuxer.Factory#DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS} and {@link + * #metadataProvider} set to {@code null}. + * + *

If the {@link #metadataProvider} is not set then the {@linkplain Metadata.Entry metadata} + * from the input file is set as it is in the output file. */ public Factory() { this( - /* maxDelayBetweenSamplesMs= */ DefaultMuxer.Factory - .DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS); + /* maxDelayBetweenSamplesMs= */ DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, + /* metadataProvider= */ null); } - /** {@link Muxer.Factory} for {@link InAppMuxer}. */ - public Factory(long maxDelayBetweenSamplesMs) { + /** + * {@link Muxer.Factory} for {@link InAppMuxer}. + * + * @param maxDelayBetweenSamplesMs See {@link Muxer#getMaxDelayBetweenSamplesMs()}. + * @param metadataProvider A {@link MetadataProvider} implementation. If the value is set to + * {@code null} then the {@linkplain Metadata.Entry metadata} from the input file is set as + * it is in the output file. + */ + public Factory(long maxDelayBetweenSamplesMs, @Nullable MetadataProvider metadataProvider) { this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs; + this.metadataProvider = metadataProvider; } @Override @@ -69,7 +102,7 @@ public final class InAppMuxer implements Muxer { } Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputStream).build(); - return new InAppMuxer(mp4Muxer, maxDelayBetweenSamplesMs); + return new InAppMuxer(mp4Muxer, maxDelayBetweenSamplesMs, metadataProvider); } @Override @@ -85,13 +118,18 @@ public final class InAppMuxer implements Muxer { private final Mp4Muxer mp4Muxer; private final long maxDelayBetweenSamplesMs; + private final @Nullable MetadataProvider metadataProvider; private final List trackTokenList; private final BufferInfo bufferInfo; private final Set metadataEntries; - private InAppMuxer(Mp4Muxer mp4Muxer, long maxDelayBetweenSamplesMs) { + private InAppMuxer( + Mp4Muxer mp4Muxer, + long maxDelayBetweenSamplesMs, + @Nullable MetadataProvider metadataProvider) { this.mp4Muxer = mp4Muxer; this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs; + this.metadataProvider = metadataProvider; trackTokenList = new ArrayList<>(); bufferInfo = new BufferInfo(); metadataEntries = new LinkedHashSet<>(); @@ -138,6 +176,7 @@ public final class InAppMuxer implements Muxer { for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); // Keep only supported metadata. + // LINT.IfChange(added_metadata) if (entry instanceof Mp4LocationData) { metadataEntries.add(entry); } @@ -146,7 +185,15 @@ 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 { mp4Muxer.close(); } catch (IOException e) { @@ -161,6 +208,7 @@ public final class InAppMuxer implements Muxer { private void writeMetadata() { for (Metadata.Entry entry : metadataEntries) { + // LINT.IfChange(written_metadata) if (entry instanceof Mp4LocationData) { mp4Muxer.setLocation( ((Mp4LocationData) entry).latitude, ((Mp4LocationData) entry).longitude); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java new file mode 100644 index 0000000000..15c06bf652 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndTest.java @@ -0,0 +1,95 @@ +/* + * 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.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.content.Context; +import android.net.Uri; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Metadata; +import androidx.media3.common.util.Util; +import androidx.media3.container.Mp4LocationData; +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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** End-to-end test for {@link Transformer} with {@link InAppMuxer}. */ +@RunWith(AndroidJUnit4.class) +public class TransformerWithInAppMuxerEndToEndTest { + private static final String MP4_FILE_ASSET_DIRECTORY = "asset:///media/"; + private static final String H264_MP4 = "mp4/sample.mp4"; + private Context context; + private String outputPath; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + outputPath = Util.createTempFile(context, "TransformerTest").getPath(); + } + + @After + public void tearDown() throws Exception { + Files.delete(Paths.get(outputPath)); + } + + @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, + metadataEntries -> { + metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData); + metadataEntries.add(new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f)); + }); + 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)); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(mediaItem) + .setEffects( + new Effects( + /* audioProcessors= */ ImmutableList.of(), + /* videoEffects= */ ImmutableList.of())) + .build(); + + transformer.start(editedMediaItem, outputPath); + TransformerTestRunner.runLooper(transformer); + + FakeExtractorOutput fakeExtractorOutput = + androidx.media3.test.utils.TestUtil.extractAllSamplesFromFilePath( + new Mp4Extractor(), checkNotNull(outputPath)); + // [xyz: latitude=45.0, longitude=-90.0] in track metadata dump. + DumpFileAsserts.assertOutput( + context, + fakeExtractorOutput, + TestUtil.getDumpFileName(H264_MP4 + ".with_location_metadata")); + } +}