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