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