1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.mediav2.cts;
18 
19 import static android.mediav2.common.cts.CodecEncoderTestBase.isMediaTypeContainerPairValid;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 
26 import android.media.MediaCodec;
27 import android.media.MediaExtractor;
28 import android.media.MediaFormat;
29 import android.media.MediaMetadataRetriever;
30 import android.media.MediaMuxer;
31 import android.os.Build;
32 import android.util.Log;
33 
34 import androidx.test.filters.LargeTest;
35 import androidx.test.filters.SmallTest;
36 import androidx.test.platform.app.InstrumentationRegistry;
37 
38 import com.android.compatibility.common.util.ApiTest;
39 import com.android.compatibility.common.util.CddTest;
40 import com.android.compatibility.common.util.FrameworkSpecificTest;
41 import com.android.compatibility.common.util.NonMainlineTest;
42 
43 import org.junit.After;
44 import org.junit.Assume;
45 import org.junit.Before;
46 import org.junit.Test;
47 import org.junit.experimental.runners.Enclosed;
48 import org.junit.runner.RunWith;
49 import org.junit.runners.Parameterized;
50 
51 import java.io.File;
52 import java.io.IOException;
53 import java.io.RandomAccessFile;
54 import java.nio.ByteBuffer;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collection;
58 import java.util.HashMap;
59 import java.util.List;
60 
61 /**
62  * MuxerTestHelper breaks a media file to elements that a muxer can use to rebuild its clone.
63  * While testing muxer, if the test doesn't use MediaCodecs class to generate elementary
64  * stream, but uses MediaExtractor, this class will be handy
65  */
66 class MuxerTestHelper {
67     private static final String LOG_TAG = MuxerTestHelper.class.getSimpleName();
68     private static final boolean ENABLE_LOGS = false;
69     // Stts values within 0.1ms(100us) difference are fudged to save too
70     // many stts entries in MPEG4Writer.
71     static final int STTS_TOLERANCE_US = 100;
72     private String mSrcPath;
73     private String mMediaType;
74     private int mTrackCount;
75     private ArrayList<MediaFormat> mFormat = new ArrayList<>();
76     private ByteBuffer mBuff;
77     private ArrayList<ArrayList<MediaCodec.BufferInfo>> mBufferInfo;
78     private HashMap<Integer, Integer> mInpIndexMap = new HashMap<>();
79     private ArrayList<Integer> mTrackIdxOrder = new ArrayList<>();
80     private int mFrameLimit;
81     // combineMedias() uses local version of this variable
82     private HashMap<Integer, Integer> mOutIndexMap = new HashMap<>();
83     private boolean mRemoveCSD;
84 
splitMediaToMuxerParameters()85     private void splitMediaToMuxerParameters() throws IOException {
86         // Set up MediaExtractor to read from the source.
87         MediaExtractor extractor = new MediaExtractor();
88         extractor.setDataSource(mSrcPath);
89 
90         // Set up MediaFormat
91         int index = 0;
92         for (int trackID = 0; trackID < extractor.getTrackCount(); trackID++) {
93             extractor.selectTrack(trackID);
94             MediaFormat format = extractor.getTrackFormat(trackID);
95             if (mRemoveCSD) {
96                 for (int i = 0; ; ++i) {
97                     String csdKey = "csd-" + i;
98                     if (format.containsKey(csdKey)) {
99                         format.removeKey(csdKey);
100                     } else {
101                         break;
102                     }
103                 }
104             }
105             if (mMediaType == null) {
106                 mTrackCount++;
107                 mFormat.add(format);
108                 mInpIndexMap.put(trackID, index++);
109             } else {
110                 String mediaType = format.getString(MediaFormat.KEY_MIME);
111                 if (mediaType != null && mediaType.equals(mMediaType)) {
112                     mTrackCount++;
113                     mFormat.add(format);
114                     mInpIndexMap.put(trackID, index);
115                     break;
116                 } else {
117                     extractor.unselectTrack(trackID);
118                 }
119             }
120         }
121 
122         if (0 == mTrackCount) {
123             extractor.release();
124             throw new IllegalArgumentException("could not find usable track in file " + mSrcPath);
125         }
126 
127         // Set up location for elementary stream
128         File file = new File(mSrcPath);
129         int bufferSize = (int) file.length();
130         bufferSize = ((bufferSize + 127) >> 7) << 7;
131         // Ideally, Sum of return values of extractor.readSampleData(...) should not exceed
132         // source file size. But in case of Vorbis, aosp extractor appends an additional 4 bytes to
133         // the data at every readSampleData() call. bufferSize <<= 1 empirically large enough to
134         // hold the excess 4 bytes per read call
135         bufferSize <<= 1;
136         mBuff = ByteBuffer.allocate(bufferSize);
137 
138         // Set up space for bufferInfo of all samples of all tracks
139         mBufferInfo = new ArrayList<>(mTrackCount);
140         for (index = 0; index < mTrackCount; index++) {
141             mBufferInfo.add(new ArrayList<MediaCodec.BufferInfo>());
142         }
143 
144         // Let MediaExtractor do its thing
145         boolean sawEOS = false;
146         int frameCount = 0;
147         int offset = 0;
148         while (!sawEOS && frameCount < mFrameLimit) {
149             int trackID;
150             MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
151             bufferInfo.offset = offset;
152             bufferInfo.size = extractor.readSampleData(mBuff, offset);
153 
154             if (bufferInfo.size < 0) {
155                 sawEOS = true;
156             } else {
157                 bufferInfo.presentationTimeUs = extractor.getSampleTime();
158                 bufferInfo.flags = extractor.getSampleFlags();
159                 trackID = extractor.getSampleTrackIndex();
160                 mTrackIdxOrder.add(trackID);
161                 mBufferInfo.get(mInpIndexMap.get(trackID)).add(bufferInfo);
162                 extractor.advance();
163                 frameCount++;
164             }
165             offset += bufferInfo.size;
166         }
167         extractor.release();
168     }
169 
registerTrack(MediaMuxer muxer)170     void registerTrack(MediaMuxer muxer) {
171         for (int trackID = 0; trackID < mTrackCount; trackID++) {
172             int dstIndex = muxer.addTrack(mFormat.get(trackID));
173             mOutIndexMap.put(trackID, dstIndex);
174         }
175     }
176 
insertSampleData(MediaMuxer muxer)177     void insertSampleData(MediaMuxer muxer) {
178         // write all registered tracks in interleaved order
179         int[] frameCount = new int[mTrackCount];
180         for (int i = 0; i < mTrackIdxOrder.size(); i++) {
181             int trackID = mTrackIdxOrder.get(i);
182             int index = mInpIndexMap.get(trackID);
183             MediaCodec.BufferInfo bufferInfo = mBufferInfo.get(index).get(frameCount[index]);
184             muxer.writeSampleData(mOutIndexMap.get(index), mBuff, bufferInfo);
185             frameCount[index]++;
186             if (ENABLE_LOGS) {
187                 Log.v(LOG_TAG, "Track: " + index + " Timestamp: " + bufferInfo.presentationTimeUs);
188             }
189         }
190         if (ENABLE_LOGS) {
191             Log.v(LOG_TAG, "Total samples: " + mTrackIdxOrder.size());
192         }
193     }
194 
muxMedia(MediaMuxer muxer)195     void muxMedia(MediaMuxer muxer) {
196         registerTrack(muxer);
197         muxer.start();
198         insertSampleData(muxer);
199         muxer.stop();
200     }
201 
combineMedias(MediaMuxer muxer, Object o, int[] repeater)202     void combineMedias(MediaMuxer muxer, Object o, int[] repeater) {
203         if (o == null || getClass() != o.getClass())
204             throw new IllegalArgumentException("Invalid Object handle");
205         if (null == repeater || repeater.length < 2)
206             throw new IllegalArgumentException("Invalid Parameter, repeater");
207         MuxerTestHelper that = (MuxerTestHelper) o;
208 
209         // add tracks
210         int totalTracksToAdd = repeater[0] * this.mTrackCount + repeater[1] * that.mTrackCount;
211         int[] outIndexMap = new int[totalTracksToAdd];
212         MuxerTestHelper[] group = {this, that};
213         for (int k = 0, idx = 0; k < group.length; k++) {
214             for (int j = 0; j < repeater[k]; j++) {
215                 for (MediaFormat format : group[k].mFormat) {
216                     outIndexMap[idx++] = muxer.addTrack(format);
217                 }
218             }
219         }
220 
221         // mux samples
222         // write all registered tracks in planar order viz all samples of a track A then all
223         // samples of track B, ...
224         muxer.start();
225         for (int k = 0, idx = 0; k < group.length; k++) {
226             for (int j = 0; j < repeater[k]; j++) {
227                 for (int i = 0; i < group[k].mTrackCount; i++) {
228                     ArrayList<MediaCodec.BufferInfo> bufInfos = group[k].mBufferInfo.get(i);
229                     for (int p = 0; p < bufInfos.size(); p++) {
230                         MediaCodec.BufferInfo bufInfo = bufInfos.get(p);
231                         muxer.writeSampleData(outIndexMap[idx], group[k].mBuff, bufInfo);
232                         if (ENABLE_LOGS) {
233                             Log.v(LOG_TAG, "Track: " + outIndexMap[idx] + " Timestamp: " +
234                                     bufInfo.presentationTimeUs);
235                         }
236                     }
237                     idx++;
238                 }
239             }
240         }
241         muxer.stop();
242     }
243 
MuxerTestHelper(String srcPath, String mediaType, int frameLimit, boolean aRemoveCSD)244     MuxerTestHelper(String srcPath, String mediaType, int frameLimit, boolean aRemoveCSD)
245             throws IOException {
246         mSrcPath = srcPath;
247         mMediaType = mediaType;
248         if (frameLimit < 0) frameLimit = Integer.MAX_VALUE;
249         mFrameLimit = frameLimit;
250         mRemoveCSD = aRemoveCSD;
251         splitMediaToMuxerParameters();
252     }
253 
MuxerTestHelper(String srcPath, String mediaType)254     MuxerTestHelper(String srcPath, String mediaType) throws IOException {
255         this(srcPath, mediaType, -1, false);
256     }
257 
MuxerTestHelper(String srcPath, int frameLimit)258     MuxerTestHelper(String srcPath, int frameLimit) throws IOException {
259         this(srcPath, null, frameLimit, false);
260     }
261 
MuxerTestHelper(String srcPath, boolean aRemoveCSD)262     MuxerTestHelper(String srcPath, boolean aRemoveCSD) throws IOException {
263         this(srcPath, null, -1, aRemoveCSD);
264     }
265 
MuxerTestHelper(String srcPath)266     MuxerTestHelper(String srcPath) throws IOException {
267         this(srcPath, null, -1, false);
268     }
269 
getTrackCount()270     int getTrackCount() {
271         return mTrackCount;
272     }
273 
274     // offset pts of samples from index sampleOffset till the end by tsOffset for each audio and
275     // video track
offsetTimeStamp(long tsAudioOffset, long tsVideoOffset, int sampleOffset)276     void offsetTimeStamp(long tsAudioOffset, long tsVideoOffset, int sampleOffset) {
277         for (int trackID = 0; trackID < mTrackCount; trackID++) {
278             long tsOffset = 0;
279             if (mFormat.get(trackID).getString(MediaFormat.KEY_MIME).startsWith("video/")) {
280                 tsOffset = tsVideoOffset;
281             } else if (mFormat.get(trackID).getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
282                 tsOffset = tsAudioOffset;
283             }
284             for (int i = sampleOffset; i < mBufferInfo.get(trackID).size(); i++) {
285                 MediaCodec.BufferInfo bufferInfo = mBufferInfo.get(trackID).get(i);
286                 bufferInfo.presentationTimeUs += tsOffset;
287             }
288         }
289     }
290 
291     // returns true if 'this' stream is a subset of 'o'. That is all tracks in current media
292     // stream are present in ref media stream
isSubsetOf(Object o)293     boolean isSubsetOf(Object o) {
294         if (this == o) return true;
295         if (o == null || getClass() != o.getClass()) return false;
296         MuxerTestHelper that = (MuxerTestHelper) o;
297         int MAX_SAMPLE_SIZE = 4 * 1024 * 1024;
298         byte[] refBuffer = new byte[MAX_SAMPLE_SIZE];
299         byte[] testBuffer = new byte[MAX_SAMPLE_SIZE];
300         for (int i = 0; i < mTrackCount; i++) {
301             MediaFormat thisFormat = mFormat.get(i);
302             String thisMediaType = thisFormat.getString(MediaFormat.KEY_MIME);
303             int j = 0;
304             for (; j < that.mTrackCount; j++) {
305                 MediaFormat thatFormat = that.mFormat.get(j);
306                 String thatMediaType = thatFormat.getString(MediaFormat.KEY_MIME);
307                 if (thisMediaType != null && thisMediaType.equals(thatMediaType)) {
308                     if (!ExtractorTest.isFormatSimilar(thisFormat, thatFormat)) continue;
309                     if (mBufferInfo.get(i).size() == that.mBufferInfo.get(j).size()) {
310                         long tolerance = thisMediaType.startsWith("video/") ? STTS_TOLERANCE_US : 0;
311                         int k = 0;
312                         for (; k < mBufferInfo.get(i).size(); k++) {
313                             MediaCodec.BufferInfo thisInfo = mBufferInfo.get(i).get(k);
314                             MediaCodec.BufferInfo thatInfo = that.mBufferInfo.get(j).get(k);
315                             if (thisInfo.flags != thatInfo.flags) {
316                                 break;
317                             }
318                             if (thisInfo.size != thatInfo.size) {
319                                 break;
320                             } else {
321                                 mBuff.position(thisInfo.offset);
322                                 mBuff.get(refBuffer, 0, thisInfo.size);
323                                 that.mBuff.position(thatInfo.offset);
324                                 that.mBuff.get(testBuffer, 0, thatInfo.size);
325                                 int count = 0;
326                                 for (; count < thisInfo.size; count++) {
327                                     if (refBuffer[count] != testBuffer[count]) {
328                                         break;
329                                     }
330                                 }
331                                 if (count != thisInfo.size) break;
332                             }
333                             if (Math.abs(
334                                     thisInfo.presentationTimeUs - thatInfo.presentationTimeUs) >
335                                     tolerance) {
336                                 break;
337                             }
338                         }
339                         // all samples are identical. successful match found. move to next track
340                         if (k == mBufferInfo.get(i).size()) break;
341                     } else {
342                         if (ENABLE_LOGS) {
343                             Log.d(LOG_TAG, "mediaType matched but sample count different."
344                                     + " Total Samples ref/test: " + mBufferInfo.get(i).size() + '/'
345                                     + that.mBufferInfo.get(j).size());
346                         }
347                     }
348                 }
349             }
350             mBuff.position(0);
351             that.mBuff.position(0);
352             if (j == that.mTrackCount) {
353                 if (ENABLE_LOGS) {
354                     Log.d(LOG_TAG, "For track: " + thisMediaType + " Couldn't find a match ");
355                 }
356                 return false;
357             }
358         }
359         return true;
360     }
361 }
362 
363 @RunWith(Enclosed.class)
364 public class MuxerTest {
365     // duplicate definitions of hide fields of MediaMuxer.OutputFormat.
366     private static final int MUXER_OUTPUT_FIRST = 0;
367     private static final int MUXER_OUTPUT_LAST = MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG;
368 
369     private static final String MUX_SEL_KEY = "mux-sel";
370     private static String selector;
371     private static boolean[] muxSelector = new boolean[MUXER_OUTPUT_LAST + 1];
372     private static HashMap<Integer, String> formatStringPair = new HashMap<>();
373 
374     static {
375         android.os.Bundle args = InstrumentationRegistry.getArguments();
376         final String defSel = "mp4;webm;3gp;ogg";
377         selector = (null == args.getString(MUX_SEL_KEY)) ? defSel : args.getString(MUX_SEL_KEY);
378 
createFormatStringPair()379         createFormatStringPair();
380         for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; format++) {
381             muxSelector[format] = selector.contains(formatStringPair.get(format));
382         }
383     }
384 
createFormatStringPair()385     static private void createFormatStringPair() {
386         formatStringPair.put(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "mp4");
387         formatStringPair.put(MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "webm");
388         formatStringPair.put(MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "3gp");
389         formatStringPair.put(MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF, "heif");
390         formatStringPair.put(MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG, "ogg");
391     }
392 
shouldRunTest(int format)393     static private boolean shouldRunTest(int format) {
394         return muxSelector[format];
395     }
396 
397     /**
398      * Tests MediaMuxer API that are dependent on MediaMuxer.OutputFormat. setLocation,
399      * setOrientationHint are dependent on the media type and OutputFormat. Legality of these APIs
400      * are tested in this class.
401      */
402     @FrameworkSpecificTest
403     @NonMainlineTest
404     @SmallTest
405     @RunWith(Parameterized.class)
406     public static class TestApi {
407         private int mOutFormat;
408         private String mSrcFile;
409         private String mInpPath;
410         private String mOutPath;
411         private int mTrackCount;
412         private static final float ANNAPURNA_LAT = 28.59f;
413         private static final float ANNAPURNA_LONG = 83.82f;
414         private static final float TOLERANCE = 0.0002f;
415         private static final int CURRENT_ROTATION = 180;
416 
417         static {
418             System.loadLibrary("ctsmediav2muxer_jni");
419         }
420 
421         @Before
prologue()422         public void prologue() throws IOException {
423             mInpPath = WorkDir.getMediaDirString() + mSrcFile;
424             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
425         }
426 
427         @After
epilogue()428         public void epilogue() {
429             new File(mOutPath).delete();
430         }
431 
432         @Parameterized.Parameters(name = "{index}_{3}")
input()433         public static Collection<Object[]> input() {
434             return Arrays.asList(new Object[][]{
435                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_cif_768kbps_30fps_avc.mp4",
436                             1, "mp4"},
437                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_cif_768kbps_30fps_vp9.mkv",
438                             1, "webm"},
439                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "bbb_cif_768kbps_30fps_h263.mp4",
440                             1, "3gpp"},
441                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG, "bbb_stereo_48kHz_192kbps_opus.ogg",
442                             1, "ogg"},
443                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
444                             "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", 2, "mp4"},
445             });
446         }
447 
TestApi(int outFormat, String srcFile, int trackCount, String testName)448         public TestApi(int outFormat, String srcFile, int trackCount, String testName) {
449             mOutFormat = outFormat;
450             mSrcFile = srcFile;
451             mTrackCount = trackCount;
452         }
453 
nativeTestSetLocation(int format, String srcPath, String outPath)454         private native boolean nativeTestSetLocation(int format, String srcPath, String outPath);
455 
nativeTestSetOrientationHint(int format, String srcPath, String outPath)456         private native boolean nativeTestSetOrientationHint(int format, String srcPath,
457                 String outPath);
458 
nativeTestGetTrackCount(String srcPath, String outPath, int outFormat, int trackCount)459         private native boolean nativeTestGetTrackCount(String srcPath, String outPath,
460                 int outFormat, int trackCount);
461 
nativeTestGetTrackFormat(String srcPath, String outPath, int outFormat)462         private native boolean nativeTestGetTrackFormat(String srcPath, String outPath,
463                 int outFormat);
464 
verifyLocationInFile(String fileName)465         private void verifyLocationInFile(String fileName) throws IOException {
466             if (mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 &&
467                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) return;
468             MediaMetadataRetriever retriever = new MediaMetadataRetriever();
469             retriever.setDataSource(fileName);
470 
471             // parsing String location and recover the location information in floats
472             // Make sure the tolerance is very small - due to rounding errors.
473 
474             // Get the position of the -/+ sign in location String, which indicates
475             // the beginning of the longitude.
476             String loc = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
477             assertTrue(loc != null);
478             int minusIndex = loc.lastIndexOf('-');
479             int plusIndex = loc.lastIndexOf('+');
480 
481             assertTrue("+ or - is not found or found only at the beginning [" + loc + "]",
482                     (minusIndex > 0 || plusIndex > 0));
483             int index = Math.max(minusIndex, plusIndex);
484 
485             float latitude = Float.parseFloat(loc.substring(0, index - 1));
486             int lastIndex = loc.lastIndexOf('/', index);
487             if (lastIndex == -1) {
488                 lastIndex = loc.length();
489             }
490             float longitude = Float.parseFloat(loc.substring(index, lastIndex - 1));
491             assertTrue("Incorrect latitude: " + latitude + " [" + loc + "]",
492                     Math.abs(latitude - ANNAPURNA_LAT) <= TOLERANCE);
493             assertTrue("Incorrect longitude: " + longitude + " [" + loc + "]",
494                     Math.abs(longitude - ANNAPURNA_LONG) <= TOLERANCE);
495             retriever.release();
496         }
497 
verifyOrientation(String fileName)498         private void verifyOrientation(String fileName) throws IOException {
499             if (mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 &&
500                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) return;
501             MediaMetadataRetriever retriever = new MediaMetadataRetriever();
502             retriever.setDataSource(fileName);
503 
504             String testDegrees = retriever.extractMetadata(
505                     MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
506             assertTrue(testDegrees != null);
507             assertEquals("Different degrees " + CURRENT_ROTATION + " and " + testDegrees,
508                     CURRENT_ROTATION, Integer.parseInt(testDegrees));
509             retriever.release();
510         }
511 
512         @ApiTest(apis = "android.media.MediaMuxer#setLocation")
513         @Test
testSetLocation()514         public void testSetLocation() throws IOException {
515             Assume.assumeTrue(shouldRunTest(mOutFormat));
516             MediaMuxer muxer = new MediaMuxer(mOutPath, mOutFormat);
517             try {
518                 boolean isGeoDataSupported = false;
519                 final float tooFarNorth = 90.5f;
520                 final float tooFarWest = -180.5f;
521                 final float tooFarSouth = -90.5f;
522                 final float tooFarEast = 180.5f;
523                 final float atlanticLat = 14.59f;
524                 final float atlanticLong = 28.67f;
525 
526                 try {
527                     muxer.setLocation(tooFarNorth, atlanticLong);
528                     fail("setLocation succeeded with bad argument: [" + tooFarNorth + "," +
529                             atlanticLong + "]");
530                 } catch (Exception e) {
531                     // expected
532                 }
533                 try {
534                     muxer.setLocation(tooFarSouth, atlanticLong);
535                     fail("setLocation succeeded with bad argument: [" + tooFarSouth + "," +
536                             atlanticLong + "]");
537                 } catch (Exception e) {
538                     // expected
539                 }
540                 try {
541                     muxer.setLocation(atlanticLat, tooFarWest);
542                     fail("setLocation succeeded with bad argument: [" + atlanticLat + "," +
543                             tooFarWest + "]");
544                 } catch (Exception e) {
545                     // expected
546                 }
547                 try {
548                     muxer.setLocation(atlanticLat, tooFarEast);
549                     fail("setLocation succeeded with bad argument: [" + atlanticLat + "," +
550                             tooFarEast + "]");
551                 } catch (Exception e) {
552                     // expected
553                 }
554                 try {
555                     muxer.setLocation(tooFarNorth, tooFarWest);
556                     fail("setLocation succeeded with bad argument: [" + tooFarNorth + "," +
557                             tooFarWest + "]");
558                 } catch (Exception e) {
559                     // expected
560                 }
561                 try {
562                     muxer.setLocation(atlanticLat, atlanticLong);
563                     isGeoDataSupported = true;
564                 } catch (Exception e) {
565                     // can happen
566                 }
567 
568                 if (isGeoDataSupported) {
569                     try {
570                         muxer.setLocation(ANNAPURNA_LAT, ANNAPURNA_LONG);
571                     } catch (IllegalArgumentException e) {
572                         fail(e.getMessage());
573                     }
574                 } else {
575                     assertTrue(mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 &&
576                             mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP);
577                 }
578 
579                 MuxerTestHelper mediaInfo = new MuxerTestHelper(mInpPath, 60);
580                 mediaInfo.registerTrack(muxer);
581                 muxer.start();
582                 // after start
583                 try {
584                     muxer.setLocation(0.0f, 0.0f);
585                     fail("SetLocation succeeded after muxer.start()");
586                 } catch (IllegalStateException e) {
587                     // Exception
588                 }
589                 mediaInfo.insertSampleData(muxer);
590                 muxer.stop();
591                 // after stop
592                 try {
593                     muxer.setLocation(ANNAPURNA_LAT, ANNAPURNA_LONG);
594                     fail("setLocation() succeeded after muxer.stop()");
595                 } catch (IllegalStateException e) {
596                     // expected
597                 }
598                 muxer.release();
599                 // after release
600                 try {
601                     muxer.setLocation(ANNAPURNA_LAT, ANNAPURNA_LONG);
602                     fail("setLocation() succeeded after muxer.release()");
603                 } catch (IllegalStateException e) {
604                     // expected
605                 }
606                 verifyLocationInFile(mOutPath);
607             } finally {
608                 muxer.release();
609             }
610         }
611 
612         @ApiTest(apis = "android.media.MediaMuxer#setOrientationHint")
613         @Test
testSetOrientationHint()614         public void testSetOrientationHint() throws IOException {
615             Assume.assumeTrue(shouldRunTest(mOutFormat));
616             MediaMuxer muxer = new MediaMuxer(mOutPath, mOutFormat);
617             try {
618                 boolean isOrientationSupported = false;
619                 final int[] badRotation = {360, 45, -90};
620                 final int oldRotation = 90;
621 
622                 for (int degree : badRotation) {
623                     try {
624                         muxer.setOrientationHint(degree);
625                         fail("setOrientationHint() succeeded with bad argument :" + degree);
626                     } catch (Exception e) {
627                         // expected
628                     }
629                 }
630                 try {
631                     muxer.setOrientationHint(oldRotation);
632                     isOrientationSupported = true;
633                 } catch (Exception e) {
634                     // can happen
635                 }
636                 if (isOrientationSupported) {
637                     try {
638                         muxer.setOrientationHint(CURRENT_ROTATION);
639                     } catch (IllegalArgumentException e) {
640                         fail(e.getMessage());
641                     }
642                 } else {
643                     assertTrue(mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 &&
644                             mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP);
645                 }
646 
647                 MuxerTestHelper mediaInfo = new MuxerTestHelper(mInpPath, 60);
648                 mediaInfo.registerTrack(muxer);
649                 muxer.start();
650                 // after start
651                 try {
652                     muxer.setOrientationHint(0);
653                     fail("setOrientationHint succeeded after muxer.start()");
654                 } catch (IllegalStateException e) {
655                     // Exception
656                 }
657                 mediaInfo.insertSampleData(muxer);
658                 muxer.stop();
659                 // after stop
660                 try {
661                     muxer.setOrientationHint(CURRENT_ROTATION);
662                     fail("setOrientationHint() succeeded after muxer.stop()");
663                 } catch (IllegalStateException e) {
664                     // expected
665                 }
666                 muxer.release();
667                 // after release
668                 try {
669                     muxer.setOrientationHint(CURRENT_ROTATION);
670                     fail("setOrientationHint() succeeded after muxer.release()");
671                 } catch (IllegalStateException e) {
672                     // expected
673                 }
674                 verifyOrientation(mOutPath);
675             } finally {
676                 muxer.release();
677             }
678         }
679 
680         @ApiTest(apis = "AMediaMuxer_setLocation")
681         @Test
testSetLocationNative()682         public void testSetLocationNative() throws IOException {
683             Assume.assumeTrue(shouldRunTest(mOutFormat));
684             assertTrue(nativeTestSetLocation(mOutFormat, mInpPath, mOutPath));
685             verifyLocationInFile(mOutPath);
686         }
687 
688         @ApiTest(apis = "AMediaMuxer_setOrientationHint")
689         @Test
testSetOrientationHintNative()690         public void testSetOrientationHintNative() throws IOException {
691             Assume.assumeTrue(shouldRunTest(mOutFormat));
692             assertTrue(nativeTestSetOrientationHint(mOutFormat, mInpPath, mOutPath));
693             verifyOrientation(mOutPath);
694         }
695 
696         @ApiTest(apis = "AMediaMuxer_getTrackCount")
697         @Test
testGetTrackCountNative()698         public void testGetTrackCountNative() {
699             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
700             assertTrue(nativeTestGetTrackCount(mInpPath, mOutPath, mOutFormat, mTrackCount));
701         }
702 
703         @ApiTest(apis = "AMediaMuxer_getTrackFormat")
704         @Test
testGetTrackFormatNative()705         public void testGetTrackFormatNative() {
706             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
707             assertTrue(nativeTestGetTrackFormat(mInpPath, mOutPath, mOutFormat));
708         }
709     }
710 
711     /**
712      * Tests muxing multiple Video/Audio Tracks
713      */
714     @FrameworkSpecificTest
715     @NonMainlineTest
716     @LargeTest
717     @RunWith(Parameterized.class)
718     public static class TestMultiTrack {
719         private int mOutFormat;
720         private String mSrcFileA;
721         private String mSrcFileB;
722         private String mInpPathA;
723         private String mInpPathB;
724         private String mRefPath;
725         private String mOutPath;
726 
727         static {
728             System.loadLibrary("ctsmediav2muxer_jni");
729         }
730 
731         @Before
prologue()732         public void prologue() throws IOException {
733             mInpPathA = WorkDir.getMediaDirString() + mSrcFileA;
734             mInpPathB = WorkDir.getMediaDirString() + mSrcFileB;
735             mRefPath = File.createTempFile("ref", ".out").getAbsolutePath();
736             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
737         }
738 
739         @After
epilogue()740         public void epilogue() {
741             new File(mRefPath).delete();
742             new File(mOutPath).delete();
743         }
744 
745         @Parameterized.Parameters(name = "{index}_{3}")
input()746         public static Collection<Object[]> input() {
747             return Arrays.asList(new Object[][]{
748                     // audio, video are 3 sec
749                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_cif_768kbps_30fps_h263" +
750                             ".mp4", "bbb_stereo_48kHz_192kbps_aac.mp4", "mp4"},
751                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_cif_768kbps_30fps_vp9.mkv"
752                             , "bbb_stereo_48kHz_192kbps_vorbis.ogg", "webm"},
753                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "bbb_cif_768kbps_30fps_h263.mp4"
754                             , "bbb_mono_16kHz_20kbps_amrwb.amr", "3gpp"},
755 
756                     // audio 3 sec, video 10 sec
757                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_qcif_512kbps_30fps_avc" +
758                             ".mp4", "bbb_stereo_48kHz_192kbps_aac.mp4", "mp4"},
759                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_qcif_512kbps_30fps_vp9.webm"
760                             , "bbb_stereo_48kHz_192kbps_vorbis.ogg", "webm"},
761                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "bbb_qcif_512kbps_30fps_h263.3gp"
762                             , "bbb_mono_16kHz_20kbps_amrwb.amr", "3gpp"},
763 
764                     // audio 10 sec, video 3 sec
765                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_cif_768kbps_30fps_h263" +
766                             ".mp4", "bbb_stereo_48kHz_128kbps_aac.mp4", "mp4"},
767                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_cif_768kbps_30fps_vp9.mkv"
768                             , "bbb_stereo_48kHz_128kbps_vorbis.ogg", "webm"},
769                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "bbb_cif_768kbps_30fps_h263.mp4"
770                             , "bbb_mono_8kHz_8kbps_amrnb.3gp", "3gpp"},
771             });
772         }
773 
TestMultiTrack(int outFormat, String srcFileA, String srcFileB, String testName)774         public TestMultiTrack(int outFormat, String srcFileA, String srcFileB, String testName) {
775             mOutFormat = outFormat;
776             mSrcFileA = srcFileA;
777             mSrcFileB = srcFileB;
778         }
779 
nativeTestMultiTrack(int format, String fileA, String fileB, String fileR, String fileO)780         private native boolean nativeTestMultiTrack(int format, String fileA, String fileB,
781                 String fileR, String fileO);
782 
783         @ApiTest(apis = {"android.media.MediaMuxer#MediaMuxer", "android.media.MediaMuxer#addTrack",
784                 "android.media.MediaMuxer#start", "android.media.MediaMuxer#writeSampleData",
785                 "android.media.MediaMuxer#stop", "android.media.MediaMuxer#release"})
786         @Test
testMultiTrack()787         public void testMultiTrack() throws IOException {
788             Assume.assumeTrue(shouldRunTest(mOutFormat));
789             Assume.assumeTrue("TODO(b/146423022)",
790                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM);
791             // number of times to repeat {mSrcFileA, mSrcFileB} in Output
792             // values should be in sync with nativeTestMultiTrack
793             final int[][] numTracks = {{2, 0}, {0, 2}, {1, 2}, {2, 1}, {2, 2}};
794 
795             MuxerTestHelper mediaInfoA = new MuxerTestHelper(mInpPathA);
796             MuxerTestHelper mediaInfoB = new MuxerTestHelper(mInpPathB);
797             assertEquals("error! unexpected track count", 1, mediaInfoA.getTrackCount());
798             assertEquals("error! unexpected track count", 1, mediaInfoB.getTrackCount());
799 
800             // prepare reference
801             RandomAccessFile refFile = new RandomAccessFile(mRefPath, "rws");
802             MediaMuxer muxer = new MediaMuxer(refFile.getFD(), mOutFormat);
803             MuxerTestHelper refInfo = null;
804             String msg = String.format("testMultiTrack: inputs: %s %s, fmt: %d ", mSrcFileA,
805                     mSrcFileB, mOutFormat);
806             try {
807                 mediaInfoA.combineMedias(muxer, mediaInfoB, new int[]{1, 1});
808                 refInfo = new MuxerTestHelper(mRefPath);
809                 if (!mediaInfoA.isSubsetOf(refInfo) || !mediaInfoB.isSubsetOf(refInfo)) {
810                     fail(msg + "error ! muxing src A and src B failed");
811                 }
812             } catch (Exception e) {
813                 if (mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG) {
814                     fail(msg + "error ! muxing src A and src B failed");
815                 }
816             } finally {
817                 muxer.release();
818                 refFile.close();
819             }
820 
821             // test multi-track
822             for (int[] numTrack : numTracks) {
823                 RandomAccessFile outFile = new RandomAccessFile(mOutPath, "rws");
824                 muxer = new MediaMuxer(outFile.getFD(), mOutFormat);
825                 try {
826                     mediaInfoA.combineMedias(muxer, mediaInfoB, numTrack);
827                     MuxerTestHelper outInfo = new MuxerTestHelper(mOutPath);
828                     if (!outInfo.isSubsetOf(refInfo)) {
829                         fail(msg + " error ! muxing src A: " + numTrack[0] + " src B: " +
830                                 numTrack[1] + "failed");
831                     }
832                 } catch (Exception e) {
833                     if (mOutFormat == MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) {
834                         fail(msg + " error ! muxing src A: " + numTrack[0] + " src B: " +
835                                 numTrack[1] + "failed");
836                     }
837                 } finally {
838                     muxer.release();
839                     outFile.close();
840                 }
841             }
842         }
843 
844         @ApiTest(apis = {"AMediaMuxer_new", "AMediaMuxer_addTrack", "AMediaMuxer_start",
845                 "AMediaMuxer_writeSampleData", "AMediaMuxer_stop", "AMediaMuxer_delete"})
846         @Test
testMultiTrackNative()847         public void testMultiTrackNative() {
848             Assume.assumeTrue(shouldRunTest(mOutFormat));
849             Assume.assumeTrue("TODO(b/146423022)",
850                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM);
851             assertTrue(nativeTestMultiTrack(mOutFormat, mInpPathA, mInpPathB, mRefPath, mOutPath));
852         }
853     }
854 
855     /**
856      * Add an offset to the presentation time of samples of a track. Mux with the added offset,
857      * validate by re-extracting the muxer output file and compare with original.
858      */
859     @FrameworkSpecificTest
860     @NonMainlineTest
861     @LargeTest
862     @RunWith(Parameterized.class)
863     public static class TestOffsetPts {
864         private String mSrcFile;
865         private int mOutFormat;
866         private int[] mOffsetIndices;
867         private String mInpPath;
868         private String mOutPath;
869 
870         static {
871             System.loadLibrary("ctsmediav2muxer_jni");
872         }
873 
874         @Before
prologue()875         public void prologue() throws IOException {
876             mInpPath = WorkDir.getMediaDirString() + mSrcFile;
877             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
878         }
879 
880         @After
epilogue()881         public void epilogue() {
882             new File(mOutPath).delete();
883         }
884 
885         @Parameterized.Parameters(name = "{index}_{3}")
input()886         public static Collection<Object[]> input() {
887             return Arrays.asList(new Object[][]{
888                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
889                             "bbb_cif_768kbps_30fps_hevc_stereo_48kHz_192kbps_aac.mp4",
890                             new int[]{0}, "mp4"},
891                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM,
892                             "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm",
893                             new int[]{0}, "webm"},
894                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP,
895                             "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp",
896                             new int[]{0}, "3gpp"},
897                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG, "bbb_stereo_48kHz_192kbps_opus.ogg",
898                             new int[]{10}, "ogg"},
899                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_cif_768kbps_30fps_avc.mp4",
900                             new int[]{6, 50, 77}, "mp4"},
901                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_cif_768kbps_30fps_vp9.mkv",
902                             new int[]{8, 44}, "webm"},
903             });
904         }
905 
TestOffsetPts(int outFormat, String file, int[] offsetIndices, String testName)906         public TestOffsetPts(int outFormat, String file, int[] offsetIndices, String testName) {
907             mOutFormat = outFormat;
908             mSrcFile = file;
909             mOffsetIndices = offsetIndices;
910         }
911 
nativeTestOffsetPts(int format, String srcFile, String dstFile, int[] offsetIndices)912         private native boolean nativeTestOffsetPts(int format, String srcFile, String dstFile,
913                 int[] offsetIndices);
914 
915         @ApiTest(apis = "android.media.MediaMuxer#writeSampleData")
916         @Test
testOffsetPresentationTime()917         public void testOffsetPresentationTime() throws IOException {
918             // values sohuld be in sync with nativeTestOffsetPts
919             final long[] OFFSET_TS_AUDIO_US = {-23220L, 0L, 200000L, 400000L};
920             final long[] OFFSET_TS_VIDEO_US = {0L, 200000L, 400000L};
921             Assume.assumeTrue(shouldRunTest(mOutFormat));
922             Assume.assumeTrue("TODO(b/148978457)",
923                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
924             Assume.assumeTrue("TODO(b/148978457)",
925                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP);
926             Assume.assumeTrue("TODO(b/146423022)",
927                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM);
928             Assume.assumeTrue("TODO(b/146421018)",
929                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG);
930             MuxerTestHelper mediaInfo = new MuxerTestHelper(mInpPath);
931             for (long audioOffsetUs : OFFSET_TS_AUDIO_US) {
932                 for (long videoOffsetUs : OFFSET_TS_VIDEO_US) {
933                     for (int i = 0; i < mOffsetIndices.length; i++) {
934                         mediaInfo.offsetTimeStamp(audioOffsetUs, videoOffsetUs, mOffsetIndices[i]);
935                     }
936                     MediaMuxer muxer = new MediaMuxer(mOutPath, mOutFormat);
937                     mediaInfo.muxMedia(muxer);
938                     muxer.release();
939                     MuxerTestHelper outInfo = new MuxerTestHelper(mOutPath);
940                     if (!outInfo.isSubsetOf(mediaInfo)) {
941                         String msg = String.format(
942                                 "testOffsetPresentationTime: inp: %s, fmt: %d, audioOffsetUs %d, " +
943                                         "videoOffsetUs %d ",
944                                 mSrcFile, mOutFormat, audioOffsetUs, videoOffsetUs);
945                         fail(msg + "error! output != input");
946                     }
947                     for (int i = mOffsetIndices.length - 1; i >= 0; i--) {
948                         mediaInfo.offsetTimeStamp(-audioOffsetUs, -videoOffsetUs,
949                                 mOffsetIndices[i]);
950                     }
951                 }
952             }
953         }
954 
955         @ApiTest(apis = "AMediaMuxer_writeSampleData")
956         @Test
testOffsetPresentationTimeNative()957         public void testOffsetPresentationTimeNative() {
958             Assume.assumeTrue(shouldRunTest(mOutFormat));
959             Assume.assumeTrue("TODO(b/148978457)",
960                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
961             Assume.assumeTrue("TODO(b/148978457)",
962                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP);
963             Assume.assumeTrue("TODO(b/146423022)",
964                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM);
965             Assume.assumeTrue("TODO(b/146421018)",
966                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG);
967             assertTrue(nativeTestOffsetPts(mOutFormat, mInpPath, mOutPath, mOffsetIndices));
968         }
969     }
970 
971     /**
972      * Tests whether appending audio and/or video data to an existing media file works in all
973      * supported append modes.
974      */
975     @ApiTest(apis = {"AMediaMuxer_append", "AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP",
976             "AMEDIAMUXER_APPEND_TO_EXISTING_DATA"})
977     @FrameworkSpecificTest
978     @NonMainlineTest
979     @LargeTest
980     @RunWith(Parameterized.class)
981     public static class TestSimpleAppend {
982         private static final String LOG_TAG = MuxerTestHelper.class.getSimpleName();
983         private String mSrcFile;
984         private String mInpPath;
985         private String mOutPath;
986         private int mOutFormat;
987         private int mTrackCount;
988 
989         static {
990             System.loadLibrary("ctsmediav2muxer_jni");
991         }
992 
993         @Before
prologue()994         public void prologue() throws IOException {
995             mInpPath = WorkDir.getMediaDirString() + mSrcFile;
996             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
997         }
998 
999         @After
epilogue()1000         public void epilogue() {
1001             new File(mOutPath).delete();
1002         }
1003 
1004         @Parameterized.Parameters(name = "{index}_{3}")
input()1005         public static Collection<Object[]> input() {
1006             return Arrays.asList(new Object[][]{
1007                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
1008                             "bbb_stereo_48kHz_128kbps_aac.mp4", 1, "mp4"},
1009                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
1010                             "bbb_1920x1080_avc_high_l42.mp4", 1, "mp4"},
1011                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
1012                             "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", 2, "mp4"},
1013                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
1014                             "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", 2, "mp4"},
1015             });
1016         }
1017 
TestSimpleAppend(int outFormat, String srcFile, int trackCount, String testName)1018         public TestSimpleAppend(int outFormat, String srcFile, int trackCount, String testName) {
1019             mOutFormat = outFormat;
1020             mSrcFile = srcFile;
1021             mTrackCount = trackCount;
1022         }
1023 
nativeTestSimpleAppend(int outFormat, String srcPath, String outPath)1024         private native boolean nativeTestSimpleAppend(int outFormat, String srcPath,
1025                 String outPath);
1026 
nativeTestAppendGetTrackCount(String srcPath, int trackCount)1027         private native boolean nativeTestAppendGetTrackCount(String srcPath, int trackCount);
1028 
nativeTestNoSamples(int outFormat, String srcPath, String outPath)1029         private native boolean nativeTestNoSamples(int outFormat, String srcPath, String outPath);
1030 
nativeTestIgnoreLastGOPAppend(int outFormat, String srcPath, String outPath)1031         private native boolean nativeTestIgnoreLastGOPAppend(int outFormat, String srcPath,
1032                 String outPath);
1033 
nativeTestAppendGetTrackFormat(String srcPath)1034         private native boolean nativeTestAppendGetTrackFormat(String srcPath);
1035 
1036         @Test
testSimpleAppendNative()1037         public void testSimpleAppendNative() {
1038             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
1039             assertTrue(nativeTestSimpleAppend(mOutFormat, mInpPath, mOutPath));
1040         }
1041 
1042         @Test
testAppendGetTrackCountNative()1043         public void testAppendGetTrackCountNative() {
1044             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
1045             assertTrue(nativeTestAppendGetTrackCount(mInpPath, mTrackCount));
1046         }
1047 
1048         @Test
testAppendNoSamplesNative()1049         public void testAppendNoSamplesNative() {
1050             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
1051             assertTrue(nativeTestNoSamples(mOutFormat, mInpPath, mOutPath));
1052         }
1053 
1054         @Test
testIgnoreLastGOPAppend()1055         public void testIgnoreLastGOPAppend() {
1056             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
1057             assertTrue(nativeTestIgnoreLastGOPAppend(mOutFormat, mInpPath, mOutPath));
1058         }
1059 
1060         @Test
testAppendGetTrackFormatNative()1061         public void testAppendGetTrackFormatNative() {
1062             Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
1063             assertTrue(nativeTestAppendGetTrackFormat(mInpPath));
1064         }
1065     }
1066 
1067 
1068     /**
1069      * Audio, Video Codecs support a variety of file-types/container formats. For example,
1070      * AAC-LC supports MPEG4, 3GPP. Vorbis supports OGG and WEBM. H.263 supports 3GPP and WEBM.
1071      * This test takes the output of a codec and muxes it in to all possible container formats.
1072      * The results are checked for inconsistencies with the requirements of CDD.
1073      */
1074     @CddTest(requirements = {"5.1.3", "5.1.8"})
1075     @FrameworkSpecificTest
1076     @NonMainlineTest
1077     @LargeTest
1078     @RunWith(Parameterized.class)
1079     public static class TestSimpleMux {
1080         private String mMediaType;
1081         private String mSrcFile;
1082         private String mInpPath;
1083         private String mOutPath;
1084 
1085         static {
1086             System.loadLibrary("ctsmediav2muxer_jni");
1087         }
1088 
TestSimpleMux(String mediaType, String srcFile, String testName)1089         public TestSimpleMux(String mediaType, String srcFile, String testName) {
1090             mMediaType = mediaType;
1091             mSrcFile = srcFile;
1092         }
1093 
1094         @Before
prologue()1095         public void prologue() throws IOException {
1096             mInpPath = WorkDir.getMediaDirString() + mSrcFile;
1097             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
1098         }
1099 
1100         @After
epilogue()1101         public void epilogue() {
1102             new File(mOutPath).delete();
1103         }
1104 
doesCodecRequireCSD(String mediaType)1105         private boolean doesCodecRequireCSD(String mediaType) {
1106             return (mediaType.equals(MediaFormat.MIMETYPE_VIDEO_AVC)
1107                     || mediaType.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)
1108                     || mediaType.equals(MediaFormat.MIMETYPE_VIDEO_MPEG4)
1109                     || mediaType.equals(MediaFormat.MIMETYPE_AUDIO_AAC));
1110         }
1111 
nativeTestSimpleMux(String srcPath, String outPath, String mediaType, String selector)1112         private native boolean nativeTestSimpleMux(String srcPath, String outPath, String mediaType,
1113                 String selector);
1114 
nativeTestSimpleAppend(String srcPath, String outPath, String mediaType, String selector)1115         private native boolean nativeTestSimpleAppend(String srcPath, String outPath,
1116                 String mediaType, String selector);
1117 
1118         @Parameterized.Parameters(name = "{index}_{2}")
input()1119         public static Collection<Object[]> input() {
1120             return Arrays.asList(new Object[][]{
1121                     // Video Codecs
1122                     {MediaFormat.MIMETYPE_VIDEO_H263,
1123                             "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", "h263"},
1124                     {MediaFormat.MIMETYPE_VIDEO_AVC,
1125                             "bbb_cif_768kbps_30fps_avc_stereo_48kHz_192kbps_vorbis.mp4", "avc"},
1126                     {MediaFormat.MIMETYPE_VIDEO_HEVC,
1127                             "bbb_cif_768kbps_30fps_hevc_stereo_48kHz_192kbps_opus.mp4", "hevc"},
1128                     {MediaFormat.MIMETYPE_VIDEO_MPEG4,
1129                             "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", "mpeg4"},
1130                     {MediaFormat.MIMETYPE_VIDEO_VP8,
1131                             "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm", "vp8"},
1132                     {MediaFormat.MIMETYPE_VIDEO_VP9,
1133                             "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm", "vp9"},
1134                     // Audio Codecs
1135                     {MediaFormat.MIMETYPE_AUDIO_AAC,
1136                             "bbb_stereo_48kHz_128kbps_aac.mp4", "aac"},
1137                     {MediaFormat.MIMETYPE_AUDIO_AMR_NB,
1138                             "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", "amrnb"},
1139                     {MediaFormat.MIMETYPE_AUDIO_AMR_WB,
1140                             "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", "amrwb"},
1141                     {MediaFormat.MIMETYPE_AUDIO_OPUS,
1142                             "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm", "opus"},
1143                     {MediaFormat.MIMETYPE_AUDIO_VORBIS,
1144                             "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm", "vorbis"},
1145                     // Metadata
1146                     {"application/gyro",
1147                             "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_non_compliant.3gp",
1148                             "gyro-non-compliant"},
1149                     {"application/gyro",
1150                             "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_compliant.3gp",
1151                             "gyro-compliant"},
1152             });
1153         }
1154 
1155         @Test
testSimpleMux()1156         public void testSimpleMux() throws IOException {
1157             Assume.assumeTrue("TODO(b/146421018)",
1158                     !mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_OPUS));
1159             Assume.assumeTrue("TODO(b/146923287)",
1160                     !mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_VORBIS));
1161             MuxerTestHelper mediaInfo = new MuxerTestHelper(mInpPath, mMediaType);
1162             assertEquals("error! unexpected track count", 1, mediaInfo.getTrackCount());
1163             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; format++) {
1164                 if (!shouldRunTest(format)) continue;
1165                 // TODO(b/146923551)
1166                 if (format == MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM) continue;
1167                 String msg = String.format("testSimpleMux: inp: %s, mediaType: %s, fmt: %d ",
1168                         mSrcFile, mMediaType, format);
1169                 MediaMuxer muxer = new MediaMuxer(mOutPath, format);
1170                 try {
1171                     mediaInfo.muxMedia(muxer);
1172                     MuxerTestHelper outInfo = new MuxerTestHelper(mOutPath);
1173                     if (!mediaInfo.isSubsetOf(outInfo)) {
1174                         fail(msg + "error! output != clone(input)");
1175                     }
1176                 } catch (Exception e) {
1177                     if (isMediaTypeContainerPairValid(mMediaType, format)) {
1178                         fail(msg + "error! incompatible mediaType and output format");
1179                     }
1180                 } finally {
1181                     muxer.release();
1182                 }
1183             }
1184         }
1185 
1186         @Test
testSimpleMuxNative()1187         public void testSimpleMuxNative() {
1188             Assume.assumeTrue("TODO(b/146421018)",
1189                     !mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_OPUS));
1190             Assume.assumeTrue("TODO(b/146923287)",
1191                     !mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_VORBIS));
1192             assertTrue(nativeTestSimpleMux(mInpPath, mOutPath, mMediaType, selector));
1193         }
1194 
1195         /* Does MediaMuxer throw IllegalStateException on missing codec specific data when required.
1196          * Check if relevant exception is thrown for AAC, AVC, HEVC, and MPEG4
1197          * codecs that require CSD in MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4.
1198          * TODO(b/156767190): Need to evaluate what all codecs need CSD and also what all formats
1199          * can contain these codecs, and add test cases accordingly.
1200          * TODO(b/156767190): Add similar tests in the native side/NDK as well.
1201          * TODO(b/156767190): Make a separate class, like TestNoCSDMux, instead of being part of
1202          * TestSimpleMux?
1203          */
1204         @ApiTest(apis = "android.media.MediaMuxer#addTrack")
1205         @Test
testNoCSDMux()1206         public void testNoCSDMux() throws IOException {
1207             Assume.assumeTrue(doesCodecRequireCSD(mMediaType));
1208             MuxerTestHelper mediaInfo = new MuxerTestHelper(mInpPath, true);
1209             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; format++) {
1210                 // TODO(b/156767190)
1211                 if(format != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) continue;
1212                 MediaMuxer muxer = new MediaMuxer(mOutPath, format);
1213                 Exception expected = null;
1214                 String msg = String.format("testNoCSDMux: inp: %s, mediaType %s, fmt: %s", mSrcFile,
1215                         mMediaType, formatStringPair.get(format));
1216                 try {
1217                     mediaInfo.muxMedia(muxer);
1218                 } catch (IllegalStateException e) {
1219                     expected = e;
1220                 } catch (Exception e) {
1221                     fail(msg + ", unexpected exception:" + e.getMessage());
1222                 } finally {
1223                     assertNotNull(msg, expected);
1224                     muxer.release();
1225                 }
1226             }
1227         }
1228     }
1229 
1230     @ApiTest(apis = {"android.media.MediaMuxer#start", "android.media.MediaMuxer#stop"})
1231     @FrameworkSpecificTest
1232     @NonMainlineTest
1233     @LargeTest
1234     @RunWith(Parameterized.class)
1235     public static class TestAddEmptyTracks {
1236         private final List<String> mMediaTypeListforTypeMp4 =
1237                 Arrays.asList(MediaFormat.MIMETYPE_VIDEO_MPEG4, MediaFormat.MIMETYPE_VIDEO_H263,
1238                         MediaFormat.MIMETYPE_VIDEO_AVC, MediaFormat.MIMETYPE_VIDEO_HEVC,
1239                         MediaFormat.MIMETYPE_AUDIO_AAC, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC,
1240                         MediaFormat.MIMETYPE_TEXT_SUBRIP);
1241         private final List<String> mMediaTypeListforTypeWebm =
1242                 Arrays.asList(MediaFormat.MIMETYPE_VIDEO_VP8, MediaFormat.MIMETYPE_VIDEO_VP9,
1243                         MediaFormat.MIMETYPE_AUDIO_VORBIS, MediaFormat.MIMETYPE_AUDIO_OPUS);
1244         private final List<String> mMediaTypeListforType3gp =
1245                 Arrays.asList(MediaFormat.MIMETYPE_VIDEO_MPEG4, MediaFormat.MIMETYPE_VIDEO_H263,
1246                         MediaFormat.MIMETYPE_VIDEO_AVC, MediaFormat.MIMETYPE_AUDIO_AAC,
1247                         MediaFormat.MIMETYPE_AUDIO_AMR_NB, MediaFormat.MIMETYPE_AUDIO_AMR_WB);
1248         private final List<String> mMediaTypeListforTypeOgg =
1249                 Arrays.asList(MediaFormat.MIMETYPE_AUDIO_OPUS);
1250         private String mMediaType;
1251         private String mOutPath;
1252 
TestAddEmptyTracks(String mediaType)1253         public TestAddEmptyTracks(String mediaType) {
1254             mMediaType = mediaType;
1255         }
1256 
1257         @Before
prologue()1258         public void prologue() throws IOException {
1259             mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
1260         }
1261 
1262         @After
epilogue()1263         public void epilogue() {
1264             new File(mOutPath).delete();
1265         }
1266 
isMediaTypeContainerPairValid(int format)1267         private boolean isMediaTypeContainerPairValid(int format) {
1268             boolean result = false;
1269             if (format == MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
1270                 result = mMediaTypeListforTypeMp4.contains(mMediaType);
1271             else if (format == MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM) {
1272                 return mMediaTypeListforTypeWebm.contains(mMediaType);
1273             } else if (format == MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) {
1274                 result = mMediaTypeListforType3gp.contains(mMediaType);
1275             } else if (format == MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG) {
1276                 result = mMediaTypeListforTypeOgg.contains(mMediaType);
1277             }
1278             return result;
1279         }
1280 
1281         @Parameterized.Parameters(name = "{index}_{0}")
input()1282         public static Collection<Object[]> input() {
1283             return Arrays.asList(new Object[][]{
1284                     // Video
1285                     {MediaFormat.MIMETYPE_VIDEO_H263},
1286                     {MediaFormat.MIMETYPE_VIDEO_AVC},
1287                     {MediaFormat.MIMETYPE_VIDEO_HEVC},
1288                     {MediaFormat.MIMETYPE_VIDEO_MPEG4},
1289                     {MediaFormat.MIMETYPE_VIDEO_VP8},
1290                     {MediaFormat.MIMETYPE_VIDEO_VP9},
1291                     // Audio
1292                     {MediaFormat.MIMETYPE_AUDIO_AAC},
1293                     {MediaFormat.MIMETYPE_AUDIO_AMR_NB},
1294                     {MediaFormat.MIMETYPE_AUDIO_AMR_WB},
1295                     {MediaFormat.MIMETYPE_AUDIO_OPUS},
1296                     {MediaFormat.MIMETYPE_AUDIO_VORBIS},
1297                     // Metadata
1298                     {MediaFormat.MIMETYPE_TEXT_SUBRIP},
1299                     // Image
1300                     {MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC}
1301             });
1302         }
1303 
1304         @Test
testEmptyVideoTrack()1305         public void testEmptyVideoTrack() {
1306             if (!mMediaType.startsWith("video/")) return;
1307             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
1308                 if (!isMediaTypeContainerPairValid(format)) continue;
1309                 if (format != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) continue;
1310                 try {
1311                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
1312                     MediaFormat mediaFormat = new MediaFormat();
1313                     mediaFormat.setString(MediaFormat.KEY_MIME, mMediaType);
1314                     mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 96);
1315                     mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 128);
1316                     mediaMuxer.addTrack(mediaFormat);
1317                     mediaMuxer.start();
1318                     mediaMuxer.stop();
1319                     mediaMuxer.release();
1320                 } catch (Exception e) {
1321                     fail("testEmptyVideoTrack : unexpected exception : " + e.getMessage());
1322                 }
1323             }
1324         }
1325 
1326         @Test
testEmptyAudioTrack()1327         public void testEmptyAudioTrack() {
1328             if (!mMediaType.startsWith("audio/")) return;
1329             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
1330                 if (format != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) continue;
1331                 if (!isMediaTypeContainerPairValid(format)) continue;
1332                 try {
1333                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
1334                     MediaFormat mediaFormat = new MediaFormat();
1335                     mediaFormat.setString(MediaFormat.KEY_MIME, mMediaType);
1336                     if (mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) {
1337                         mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 16000);
1338                     } else {
1339                         mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 8000);
1340                     }
1341                     mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
1342                     mediaMuxer.addTrack(mediaFormat);
1343                     mediaMuxer.start();
1344                     mediaMuxer.stop();
1345                     mediaMuxer.release();
1346                 } catch (Exception e) {
1347                     fail("testEmptyAudioTrack : unexpected exception : " + e.getMessage());
1348                 }
1349             }
1350         }
1351 
1352         @Test
testEmptyMetaDataTrack()1353         public void testEmptyMetaDataTrack() {
1354             if (!mMediaType.startsWith("application/")) return;
1355             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
1356                 if (!isMediaTypeContainerPairValid(format)) continue;
1357                 try {
1358                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
1359                     MediaFormat mediaFormat = new MediaFormat();
1360                     mediaFormat.setString(MediaFormat.KEY_MIME, mMediaType);
1361                     mediaMuxer.addTrack(mediaFormat);
1362                     mediaMuxer.start();
1363                     mediaMuxer.stop();
1364                     mediaMuxer.release();
1365                 } catch (Exception e) {
1366                     fail("testEmptyMetaDataTrack : unexpected exception : " + e.getMessage());
1367                 }
1368             }
1369         }
1370 
1371         @Test
testEmptyImageTrack()1372         public void testEmptyImageTrack() {
1373             if (!mMediaType.startsWith("image/")) return;
1374             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
1375                 if (!isMediaTypeContainerPairValid(format)) continue;
1376                 try {
1377                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
1378                     MediaFormat mediaFormat = new MediaFormat();
1379                     mediaFormat.setString(MediaFormat.KEY_MIME, mMediaType);
1380                     mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 96);
1381                     mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 128);
1382                     mediaMuxer.addTrack(mediaFormat);
1383                     mediaMuxer.start();
1384                     mediaMuxer.stop();
1385                     mediaMuxer.release();
1386                 } catch (Exception e) {
1387                     fail("testEmptyImageTrack : unexpected exception : " + e.getMessage());
1388                 }
1389             }
1390         }
1391     }
1392 }
1393