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