Add support for updating Metadata entries via InAppMuxer

Mp4Muxer already supports writing Mp4LocationData so added that
as supported Metadata entry.
Support for more Metadata entries will be added in upcoming CLs.

PiperOrigin-RevId: 534473866
This commit is contained in:
sheenachhabra 2023-05-23 18:33:12 +01:00 committed by tonihei
parent 9a79571284
commit 7c477589e5
3 changed files with 489 additions and 7 deletions

View File

@ -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

View File

@ -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 {
/** {@link Muxer.Factory} for {@link InAppMuxer}. */
public static final class Factory implements Muxer.Factory {
private final long maxDelayBetweenSamplesMs;
/** Provides {@linkplain Metadata.Entry metadata} to add in the output MP4 file. */
public interface MetadataProvider {
/**
* Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs}
* set to {@link DefaultMuxer.Factory#DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS}.
* Updates the list of {@link 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}:
*
* <ul>
* <li>{@link Mp4LocationData}
* </ul>
*/
public Factory() {
this(
/* maxDelayBetweenSamplesMs= */ DefaultMuxer.Factory
.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS);
void updateMetadataEntries(Set<Metadata.Entry> metadataEntries);
}
/** {@link Muxer.Factory} for {@link InAppMuxer}. */
public Factory(long maxDelayBetweenSamplesMs) {
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} and {@link
* #metadataProvider} set to {@code null}.
*
* <p>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,
/* metadataProvider= */ null);
}
/**
* {@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<TrackToken> trackTokenList;
private final BufferInfo bufferInfo;
private final Set<Metadata.Entry> 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<Metadata.Entry> 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);

View File

@ -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"));
}
}