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