1 /*
2  * Copyright (C) 2022 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.media.muxer.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertTrue;
21 
22 import android.content.res.AssetFileDescriptor;
23 import android.media.MediaExtractor;
24 import android.media.MediaFormat;
25 import android.media.MediaPlayer;
26 import android.media.cts.MediaTestBase;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.ParcelFileDescriptor;
30 import android.platform.test.annotations.AppModeFull;
31 import android.platform.test.annotations.Presubmit;
32 import android.platform.test.annotations.RequiresDevice;
33 import android.util.Log;
34 
35 import androidx.test.ext.junit.runners.AndroidJUnit4;
36 import androidx.test.filters.SmallTest;
37 
38 import com.android.compatibility.common.util.ApiLevelUtil;
39 import com.android.compatibility.common.util.FrameworkSpecificTest;
40 import com.android.compatibility.common.util.MediaUtils;
41 import com.android.compatibility.common.util.NonMainlineTest;
42 import com.android.compatibility.common.util.Preconditions;
43 
44 import org.junit.After;
45 import org.junit.Before;
46 import org.junit.Ignore;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.io.File;
51 import java.io.FileNotFoundException;
52 import java.nio.ByteBuffer;
53 import java.util.Set;
54 
55 @FrameworkSpecificTest
56 @NonMainlineTest
57 @SmallTest
58 @RequiresDevice
59 @AppModeFull(reason = "TODO: evaluate and port to instant")
60 @RunWith(AndroidJUnit4.class)
61 public class NativeMuxerTest extends MediaTestBase {
62     private static final String TAG = "NativeMuxerTest";
63 
64     private static final boolean sIsAtLeastS = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
65 
66     private static final String MEDIA_DIR = WorkDir.getMediaDirString();
67 
68     static {
69         // Load jni on initialization.
70         Log.i("@@@", "before loadlibrary");
71         System.loadLibrary("ctsmediamuxertest_jni");
72         Log.i("@@@", "after loadlibrary");
73     }
74 
75     @Before
76     @Override
setUp()77     public void setUp() throws Throwable {
78         super.setUp();
79     }
80 
81     @After
82     @Override
tearDown()83     public void tearDown() {
84         super.tearDown();
85     }
86 
getAssetFileDescriptorFor(final String res)87     private static AssetFileDescriptor getAssetFileDescriptorFor(final String res)
88             throws FileNotFoundException {
89         Preconditions.assertTestFileExists(MEDIA_DIR + res);
90         File inpFile = new File(MEDIA_DIR + res);
91         ParcelFileDescriptor parcelFD =
92                 ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY);
93         return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
94     }
95 
96     // check that native extractor behavior matches java extractor
97     @Presubmit
98     @Test
testMuxerAvc()99     public void testMuxerAvc() throws Exception {
100         // IMPORTANT: this file must not have B-frames
101         testMuxer("video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
102     }
103 
104     @Test
testMuxerH263()105     public void testMuxerH263() throws Exception {
106         // IMPORTANT: this file must not have B-frames
107         testMuxer("video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz.3gp");
108     }
109 
110     @Test
testMuxerHevc()111     public void testMuxerHevc() throws Exception {
112         // IMPORTANT: this file must not have B-frames
113         testMuxer("video_640x360_mp4_hevc_450kbps_no_b.mp4");
114     }
115 
116     @Test
testMuxerVp8()117     public void testMuxerVp8() throws Exception {
118         testMuxer("bbb_s1_640x360_webm_vp8_2mbps_30fps_vorbis_5ch_320kbps_48000hz.webm");
119     }
120 
121     @Test
testMuxerVp9()122     public void testMuxerVp9() throws Exception {
123         testMuxer("video_1280x720_webm_vp9_csd_309kbps_25fps_vorbis_stereo_128kbps_48000hz.webm");
124     }
125 
126     @Test
testMuxerVp9NoCsd()127     public void testMuxerVp9NoCsd() throws Exception {
128         testMuxer("bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm");
129     }
130 
131     @Test
testMuxerVp9Hdr()132     public void testMuxerVp9Hdr() throws Exception {
133         testMuxer("video_256x144_webm_vp9_hdr_83kbps_24fps.webm");
134     }
135 
136     // We do not support MPEG-2 muxing as of yet
137     @Ignore
138     @Test
SKIP_testMuxerMpeg2()139     public void SKIP_testMuxerMpeg2() throws Exception {
140         // IMPORTANT: this file must not have B-frames
141         testMuxer("video_176x144_mp4_mpeg2_105kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
142     }
143 
144     @Test
testMuxerMpeg4()145     public void testMuxerMpeg4() throws Exception {
146         // IMPORTANT: this file must not have B-frames
147         testMuxer("video_176x144_mp4_mpeg4_300kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
148     }
149 
150     @Test
testMuxerAv1()151     public void testMuxerAv1() throws Exception {
152         testMuxer("video_1280x720_mp4_av1_2000kbps_30fps_aac_stereo_128kbps_44100hz.mp4", true);
153     }
154 
testMuxer(final String res)155     private void testMuxer(final String res) throws Exception {
156         testMuxer(res, false);
157     }
158 
testMuxer(final String res, boolean signalEos)159     private void testMuxer(final String res, boolean signalEos) throws Exception {
160         boolean webm = res.endsWith("webm");
161         Preconditions.assertTestFileExists(MEDIA_DIR + res);
162         if (!MediaUtils.checkCodecsForResource(MEDIA_DIR + res)) {
163             return; // skip
164         }
165 
166         AssetFileDescriptor infd = getAssetFileDescriptorFor(res);
167 
168         File base = mContext.getExternalFilesDir(null);
169         String tmpFile = base.getPath() + "/tmp.dat";
170         Log.i("@@@", "using tmp file " + tmpFile);
171         new File(tmpFile).delete();
172         ParcelFileDescriptor out = ParcelFileDescriptor.open(new File(tmpFile),
173                 ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE);
174 
175         assertTrue(
176                 "muxer failed",
177                 testMuxerNative(
178                         infd.getParcelFileDescriptor().getFd(),
179                         infd.getStartOffset(),
180                         infd.getLength(),
181                         out.getFd(),
182                         webm,
183                         signalEos));
184 
185         // compare the original with the remuxed
186         MediaExtractor org = new MediaExtractor();
187         org.setDataSource(infd.getFileDescriptor(),
188                 infd.getStartOffset(), infd.getLength());
189 
190         MediaExtractor remux = new MediaExtractor();
191         remux.setDataSource(out.getFileDescriptor());
192 
193         assertEquals("mismatched numer of tracks", org.getTrackCount(), remux.getTrackCount());
194         // allow duration mismatch for webm files as ffmpeg does not consider the duration of the
195         // last frame while libwebm (and our framework) does.
196         final long maxDurationDiffUs = webm ? 50000 : 0; // 50ms for webm
197         for (int i = 0; i < org.getTrackCount(); i++) {
198             MediaFormat format1 = org.getTrackFormat(i);
199             MediaFormat format2 = remux.getTrackFormat(i);
200             Log.i("@@@", "org: " + format1);
201             Log.i("@@@", "remux: " + format2);
202             assertTrue("different formats", compareFormats(format1, format2, maxDurationDiffUs));
203         }
204 
205         org.release();
206         remux.release();
207 
208         Preconditions.assertTestFileExists(MEDIA_DIR + res);
209         MediaPlayer player1 =
210                 MediaPlayer.create(mContext, Uri.fromFile(new File(MEDIA_DIR + res)));
211         MediaPlayer player2 = MediaPlayer.create(mContext, Uri.parse("file://" + tmpFile));
212         assertEquals("duration is different",
213                 player1.getDuration(), player2.getDuration(), maxDurationDiffUs * 0.001);
214         player1.release();
215         player2.release();
216         new File(tmpFile).delete();
217     }
218 
hexString(ByteBuffer buf)219     private String hexString(ByteBuffer buf) {
220         if (buf == null) {
221             return "(null)";
222         }
223         final char[] digits =
224                 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
225 
226         StringBuilder hex = new StringBuilder();
227         for (int i = buf.position(); i < buf.limit(); ++i) {
228             byte c = buf.get(i);
229             hex.append(digits[(c >> 4) & 0xf]);
230             hex.append(digits[c & 0xf]);
231         }
232         return hex.toString();
233     }
234 
235     /**
236      * returns: null if key is in neither formats, true if they match and false otherwise
237      */
compareByteBufferInFormats(MediaFormat f1, MediaFormat f2, String key)238     private Boolean compareByteBufferInFormats(MediaFormat f1, MediaFormat f2, String key) {
239         ByteBuffer bufF1 = f1.containsKey(key) ? f1.getByteBuffer(key) : null;
240         ByteBuffer bufF2 = f2.containsKey(key) ? f2.getByteBuffer(key) : null;
241         if (bufF1 == null && bufF2 == null) {
242             return null;
243         }
244         if (bufF1 == null || !bufF1.equals(bufF2)) {
245             Log.i("@@@", "org " + key + ": " + hexString(bufF1));
246             Log.i("@@@", "rmx " + key + ": " + hexString(bufF2));
247             return false;
248         }
249         return true;
250     }
251 
compareFormats(MediaFormat f1, MediaFormat f2, long maxDurationDiffUs)252     private boolean compareFormats(MediaFormat f1, MediaFormat f2, long maxDurationDiffUs) {
253         final String KEY_DURATION = MediaFormat.KEY_DURATION;
254 
255         // allow some difference in durations
256         if (maxDurationDiffUs > 0
257                 && f1.containsKey(KEY_DURATION) && f2.containsKey(KEY_DURATION)
258                 && Math.abs(f1.getLong(KEY_DURATION)
259                 - f2.getLong(KEY_DURATION)) <= maxDurationDiffUs) {
260             f2.setLong(KEY_DURATION, f1.getLong(KEY_DURATION));
261         }
262 
263         // verify hdr-static-info
264         if (Boolean.FALSE.equals(compareByteBufferInFormats(f1, f2, "hdr-static-info"))) {
265             return false;
266         }
267 
268         // verify CSDs
269         for (int i = 0; ; ++i) {
270             String key = "csd-" + i;
271             Boolean match = compareByteBufferInFormats(f1, f2, key);
272             if (match == null) {
273                 break;
274             } else if (!match) {
275                 return false;
276             }
277         }
278 
279         // before S, mpeg4 writers jammed a fixed SAR value into the output;
280         // this was fixed in S
281         if (!sIsAtLeastS) {
282             if (f1.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT)
283                     && f2.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT)) {
284                 f2.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT,
285                         f1.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT));
286             }
287             if (f1.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH)
288                     && f2.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH)) {
289                 f2.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH,
290                         f1.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH));
291             }
292         }
293 
294         // look for f2 (the new) being a superset (>=) of f1 (the original)
295         // ensure that all of our fields in f1 appear in f2 with the same
296         // value. We allow f2 to contain extra fields.
297         Set<String> keys = f1.getKeys();
298         for (String key : keys) {
299             if (key == null) {
300                 continue;
301             }
302             if (!f2.containsKey(key)) {
303                 return false;
304             }
305             int f1Type = f1.getValueTypeForKey(key);
306             if (f1Type != f2.getValueTypeForKey(key)) {
307                 return false;
308             }
309             switch (f1Type) {
310                 case MediaFormat.TYPE_INTEGER:
311                     int f1Int = f1.getInteger(key);
312                     int f2Int = f2.getInteger(key);
313                     if (f1Int != f2Int) {
314                         return false;
315                     }
316                     break;
317                 case MediaFormat.TYPE_LONG:
318                     long f1Long = f1.getLong(key);
319                     long f2Long = f2.getLong(key);
320                     if (f1Long != f2Long) {
321                         return false;
322                     }
323                     break;
324                 case MediaFormat.TYPE_FLOAT:
325                     float f1Float = f1.getFloat(key);
326                     float f2Float = f2.getFloat(key);
327                     if (f1Float != f2Float) {
328                         return false;
329                     }
330                     break;
331                 case MediaFormat.TYPE_STRING:
332                     String f1String = f1.getString(key);
333                     String f2String = f2.getString(key);
334                     if (!f1String.equals(f2String)) {
335                         return false;
336                     }
337                     break;
338                 case MediaFormat.TYPE_BYTE_BUFFER:
339                     ByteBuffer f1ByteBuffer = f1.getByteBuffer(key);
340                     ByteBuffer f2ByteBuffer = f2.getByteBuffer(key);
341                     if (!f1ByteBuffer.equals(f2ByteBuffer)) {
342                         return false;
343                     }
344                     break;
345                 default:
346                     return false;
347             }
348         }
349 
350         // repeat for getFeatures
351         // (which we don't use in this test, but include for completeness)
352         Set<String> features = f1.getFeatures();
353         for (String key : features) {
354             if (key == null) {
355                 continue;
356             }
357             if (!f2.containsKey(key)) {
358                 return false;
359             }
360             int f1Type = f1.getValueTypeForKey(key);
361             if (f1Type != f2.getValueTypeForKey(key)) {
362                 return false;
363             }
364             switch (f1Type) {
365                 case MediaFormat.TYPE_INTEGER:
366                     int f1Int = f1.getInteger(key);
367                     int f2Int = f2.getInteger(key);
368                     if (f1Int != f2Int) {
369                         return false;
370                     }
371                     break;
372                 case MediaFormat.TYPE_LONG:
373                     long f1Long = f1.getLong(key);
374                     long f2Long = f2.getLong(key);
375                     if (f1Long != f2Long) {
376                         return false;
377                     }
378                     break;
379                 case MediaFormat.TYPE_FLOAT:
380                     float f1Float = f1.getFloat(key);
381                     float f2Float = f2.getFloat(key);
382                     if (f1Float != f2Float) {
383                         return false;
384                     }
385                     break;
386                 case MediaFormat.TYPE_STRING:
387                     String f1String = f1.getString(key);
388                     String f2String = f2.getString(key);
389                     if (!f1String.equals(f2String)) {
390                         return false;
391                     }
392                     break;
393                 case MediaFormat.TYPE_BYTE_BUFFER:
394                     ByteBuffer f1ByteBuffer = f1.getByteBuffer(key);
395                     ByteBuffer f2ByteBuffer = f2.getByteBuffer(key);
396                     if (!f1ByteBuffer.equals(f2ByteBuffer)) {
397                         return false;
398                     }
399                     break;
400                 default:
401                     return false;
402             }
403         }
404 
405         // not otherwise disqualified
406         return true;
407     }
408 
testMuxerNative( int in, long inoffset, long insize, int out, boolean webm, boolean signaleos)409     private static native boolean testMuxerNative(
410             int in, long inoffset, long insize, int out, boolean webm, boolean signaleos);
411 }
412