1 /*
2  * Copyright (C) 2017 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.cts.bitstreams.app;
18 
19 import android.app.Instrumentation;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.content.SharedPreferences.Editor;
23 import android.media.MediaCodec;
24 import android.media.MediaCodecInfo.CodecProfileLevel;
25 import android.media.MediaExtractor;
26 import android.media.MediaFormat;
27 import android.media.cts.bitstreams.MediaBitstreams;
28 import android.os.Bundle;
29 import android.os.Debug;
30 import android.os.Environment;
31 import android.util.Xml;
32 
33 import androidx.test.InstrumentationRegistry;
34 
35 import com.android.compatibility.common.util.DynamicConfigDeviceSide;
36 import com.android.compatibility.common.util.MediaUtils;
37 
38 import org.junit.BeforeClass;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.junit.runners.JUnit4;
42 import org.xmlpull.v1.XmlSerializer;
43 
44 import java.io.ByteArrayOutputStream;
45 import java.io.File;
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.OutputStream;
50 import java.io.PrintStream;
51 import java.nio.file.Files;
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Scanner;
55 import java.util.concurrent.Callable;
56 import java.util.concurrent.ExecutorService;
57 import java.util.concurrent.Executors;
58 import java.util.concurrent.Future;
59 import java.util.concurrent.TimeUnit;
60 
61 /**
62  * Test class that uses device-side media APIs to determine up to which resolution MediaPreparer
63  * should copy media files for CtsMediaStressTestCases.
64  */
65 @RunWith(JUnit4.class)
66 public class MediaBitstreamsDeviceSideTest {
67 
68     private static final String KEY_SIZE = "size";
69     private static final String UTF_8 = "utf-8";
70     /** Instrumentation status code used to write resolution to metrics */
71     private static final int INST_STATUS_IN_PROGRESS = 2;
72 
73     private static File mAppCache = InstrumentationRegistry.getContext().getExternalCacheDir();
74     private static String mDeviceBitstreamsPath = InstrumentationRegistry.getArguments().getString(
75             MediaBitstreams.OPT_DEVICE_BITSTREAMS_PATH,
76             MediaBitstreams.DEFAULT_DEVICE_BITSTEAMS_PATH);
77 
78     @BeforeClass
setUp()79     public static void setUp() {
80         Bundle args = InstrumentationRegistry.getArguments();
81         String debugStr = args.getString(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, "false");
82         boolean debug = Boolean.parseBoolean(debugStr);
83         if (debug && !Debug.isDebuggerConnected()) {
84             Debug.waitForDebugger();
85         }
86     }
87 
fixFormat(MediaFormat format, String path)88     private static void fixFormat(MediaFormat format, String path) {
89         // TODO(b/137684344): Revisit so that we can get this information from
90         //                    the bitstream or the extractor.
91         if (path.indexOf("/10bit/") < 0) {
92             return;
93         }
94         String mime = format.getString(MediaFormat.KEY_MIME);
95         int profile = -1, level = -1;
96         if (mime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) {
97             profile = CodecProfileLevel.VP9Profile2;
98             level = CodecProfileLevel.VP9Level1;
99         } else if (mime.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
100             profile = CodecProfileLevel.HEVCProfileMain10;
101             level = CodecProfileLevel.HEVCMainTierLevel1;
102         } else if (mime.equals(MediaFormat.MIMETYPE_VIDEO_AV1)) {
103             profile = CodecProfileLevel.AV1ProfileMain10;
104             level = CodecProfileLevel.AV1Level2;
105         } else {
106             return;
107         }
108 
109         if (!format.containsKey(MediaFormat.KEY_PROFILE)) {
110             format.setInteger(MediaFormat.KEY_PROFILE, profile);
111         }
112         if (!format.containsKey(MediaFormat.KEY_LEVEL)) {
113             format.setInteger(MediaFormat.KEY_LEVEL, level);
114         }
115     }
116 
117     static interface ReportCallback {
run(OutputStream out)118         void run(OutputStream out) throws Exception;
119     }
120 
121     static class GenerateBitstreamsFormatsXml implements ReportCallback {
122         @Override
run(OutputStream out)123         public void run(OutputStream out) throws Exception {
124 
125             String[] keys = new String[] {
126                     MediaFormat.KEY_WIDTH,
127                     MediaFormat.KEY_HEIGHT,
128                     MediaFormat.KEY_FRAME_RATE,
129                     MediaFormat.KEY_PROFILE,
130                     MediaFormat.KEY_LEVEL,
131                     MediaFormat.KEY_BIT_RATE};
132 
133             XmlSerializer formats = Xml.newSerializer();
134             formats.setOutput(out, UTF_8);
135             formats.startDocument(UTF_8, true);
136             formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG);
137 
138             DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE);
139             for (String path : config.keySet()) {
140 
141                 formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG_ENTRY);
142                 formats.attribute(null, MediaBitstreams.DYNAMIC_CONFIG_KEY, path);
143                 formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG_VALUE);
144 
145                 String formatStr = config.getValue(path);
146                 if (formatStr != null && !formatStr.isEmpty()) {
147                     formats.text(formatStr);
148                 } else {
149                     File media = new File(mDeviceBitstreamsPath, path);
150                     String fullPath = media.getPath();
151                     MediaFormat format = MediaUtils.getTrackFormatForPath(null, fullPath, "video");
152                     StringBuilder formatStringBuilder = new StringBuilder(MediaFormat.KEY_MIME);
153                     formatStringBuilder.append('=').append(format.getString(MediaFormat.KEY_MIME));
154                     formatStringBuilder.append(',').append(KEY_SIZE)
155                             .append('=').append(media.length());
156                     for (String key : keys) {
157                         formatStringBuilder.append(',').append(key).append('=');
158                         if (format.containsKey(key)) {
159                             formatStringBuilder.append(format.getInteger(key));
160                         }
161                     }
162                     formats.text(formatStringBuilder.toString());
163                 }
164 
165                 formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG_VALUE);
166                 formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG_ENTRY);
167 
168             }
169 
170             formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG);
171             formats.endDocument();
172 
173         }
174     }
175 
176     static class GenerateSupportedBitstreamsFormatsTxt implements ReportCallback {
177 
178         @Override
run(OutputStream out)179         public void run(OutputStream out) throws Exception {
180 
181             PrintStream ps = new PrintStream(out);
182             Bundle args = InstrumentationRegistry.getArguments();
183             String prefix = args.getString(MediaBitstreams.OPT_BITSTREAMS_PREFIX, "");
184             DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE);
185 
186             for (String path : config.keySet()) {
187 
188                 if (!path.startsWith(prefix)) {
189                     continue;
190                 }
191 
192                 String formatStr = config.getValue(path);
193                 if (formatStr == null || formatStr.isEmpty()) {
194                     continue;
195                 }
196 
197                 MediaFormat format = parseTrackFormat(formatStr);
198                 String mime = format.getString(MediaFormat.KEY_MIME);
199                 String[] decoders = MediaUtils.getDecoderNamesForMime(mime);
200                 fixFormat(format, path);
201 
202                 ps.println(path);
203                 ps.println(decoders.length);
204                 for (String name : decoders) {
205                     ps.println(name);
206                     ps.println(MediaUtils.supports(name, format));
207                 }
208 
209             }
210 
211             ps.flush();
212         }
213     }
214 
215     static class TestBitstreamsConformance implements ReportCallback {
216 
217         ExecutorService mExecutorService;
218 
getSettings()219         private SharedPreferences getSettings() {
220             Context ctx = InstrumentationRegistry.getContext();
221             SharedPreferences settings = ctx.getSharedPreferences(MediaBitstreams.K_MODULE, 0);
222             return settings;
223         }
224 
setup()225         private void setup() {
226             Bundle args = InstrumentationRegistry.getArguments();
227             String lastCrash = args.getString(MediaBitstreams.OPT_LAST_CRASH);
228             if (lastCrash != null) {
229                 SharedPreferences settings = getSettings();
230                 int n = settings.getInt(lastCrash, 0);
231                 Editor editor = settings.edit();
232                 editor.putInt(lastCrash, n + 1);
233                 editor.commit();
234             }
235         }
236 
237         @Override
run(OutputStream out)238         public void run(OutputStream out) throws Exception {
239             setup();
240             mExecutorService = Executors.newFixedThreadPool(3);
241             try (
242                 Scanner sc = new Scanner(
243                         new File(mDeviceBitstreamsPath, MediaBitstreams.K_BITSTREAMS_LIST_TXT));
244                 PrintStream ps = new PrintStream(out, true)
245             ) {
246                 while (sc.hasNextLine()) {
247                     verifyBitstream(ps, sc.nextLine());
248                 }
249             } finally {
250                 mExecutorService.shutdown();
251             }
252         }
253 
getDecodersForPath(String path)254         private List<String> getDecodersForPath(String path) throws IOException {
255             List<String> decoders = new ArrayList<>();
256             MediaExtractor ex = new MediaExtractor();
257             try {
258                 ex.setDataSource(path);
259                 MediaFormat format = ex.getTrackFormat(0);
260                 fixFormat(format, path);
261                 boolean[] vendors = new boolean[] {false, true};
262                 for (boolean v : vendors) {
263                     for (String name : MediaUtils.getDecoderNames(v, format)) {
264                         decoders.add(name);
265                     }
266                 }
267             } finally {
268                 ex.release();
269             }
270             return decoders;
271         }
272 
getFrameChecksumsForPath(String path)273         private List<String> getFrameChecksumsForPath(String path) throws IOException {
274             String md5Path = MediaBitstreams.getMd5Path(path);
275             List<String> frameMD5Sums = Files.readAllLines(
276                     new File(mDeviceBitstreamsPath, md5Path).toPath());
277             for (int i = 0; i < frameMD5Sums.size(); i++) {
278                 String line = frameMD5Sums.get(i);
279                 frameMD5Sums.set(i, line.split(" ")[0]);
280             }
281             return frameMD5Sums;
282         }
283 
verifyBitstream(PrintStream ps, String relativePath)284         private void verifyBitstream(PrintStream ps, String relativePath) {
285             ps.println(relativePath);
286 
287             List<String> decoders = new ArrayList<>();
288             List<String> frameChecksums = new ArrayList<>();
289             SharedPreferences settings = getSettings();
290             String fullPath = new File(mDeviceBitstreamsPath, relativePath).toString();
291             try {
292                 String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, "");
293                 if (settings.getInt(lastCrash, 0) >= 3) {
294                     ps.println(MediaBitstreams.K_NATIVE_CRASH);
295                     return;
296                 }
297                 decoders = getDecodersForPath(fullPath);
298                 frameChecksums = getFrameChecksumsForPath(relativePath);
299                 ps.println(false);
300             } catch (Exception e) {
301                 ps.println(true);
302                 ps.println(e.toString());
303                 return;
304             }
305 
306             ps.println(decoders.size());
307             for (String name : decoders) {
308                 ps.println(name);
309                 String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, name);
310                 if (settings.getInt(lastCrash, 0) >= 3) {
311                     ps.println(MediaBitstreams.K_NATIVE_CRASH);
312                 } else {
313                     ps.println(verifyBitstream(fullPath, name, frameChecksums));
314                 }
315             }
316 
317         }
318 
verifyBitstream(String path, String name, List<String> frameChecksums)319         private String verifyBitstream(String path, String name, List<String> frameChecksums)  {
320             MediaExtractor ex = new MediaExtractor();
321             MediaCodec d = null;
322             try {
323                 Future<MediaCodec> dec = mExecutorService.submit(new Callable<MediaCodec>() {
324                     @Override
325                     public MediaCodec call() throws Exception {
326                         return MediaCodec.createByCodecName(name);
327                     }
328                 });
329                 MediaCodec decoder = d = dec.get(1, TimeUnit.SECONDS);
330                 Future<Boolean> conform = mExecutorService.submit(new Callable<Boolean>() {
331                     @Override
332                     public Boolean call() throws Exception {
333                         ex.setDataSource(path);
334                         ex.selectTrack(0);
335                         ex.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC);
336                         return MediaUtils.verifyDecoder(decoder, ex, frameChecksums);
337                     }
338                 });
339                 return conform.get(15, TimeUnit.SECONDS).toString();
340             } catch (Exception e) {
341                 return e.toString().replaceAll("\\R", " ");
342             } finally {
343                 ex.release();
344                 if (d != null) {
345                     d.release();
346                 }
347             }
348         }
349 
350     }
351 
generateReportFile(String suffix, String reportKey, ReportCallback callback)352     private void generateReportFile(String suffix, String reportKey, ReportCallback callback)
353             throws IOException, FileNotFoundException, Exception {
354 
355         OutputStream out = new ByteArrayOutputStream(0);
356 
357         try {
358 
359             File tmpf = File.createTempFile(getClass().getSimpleName(), suffix, Environment.getExternalStorageDirectory());
360             Instrumentation inst = InstrumentationRegistry.getInstrumentation();
361             Bundle bundle = new Bundle();
362             bundle.putString(MediaBitstreams.KEY_APP_CACHE_DIR, mAppCache.getCanonicalPath());
363             bundle.putString(reportKey, tmpf.getCanonicalPath());
364             inst.sendStatus(INST_STATUS_IN_PROGRESS, bundle);
365 
366             out = new FileOutputStream(tmpf);
367             callback.run(out);
368             out.flush();
369 
370         } finally {
371 
372             out.close();
373 
374         }
375     }
376 
377     @Test
testGetBitstreamsFormats()378     public void testGetBitstreamsFormats() throws Exception {
379         generateReportFile(".xml",
380                 MediaBitstreams.KEY_BITSTREAMS_FORMATS_XML,
381                 new GenerateBitstreamsFormatsXml());
382     }
383 
384     @Test
testGetSupportedBitstreams()385     public void testGetSupportedBitstreams() throws Exception {
386         generateReportFile(".txt",
387                 MediaBitstreams.KEY_SUPPORTED_BITSTREAMS_TXT,
388                 new GenerateSupportedBitstreamsFormatsTxt());
389     }
390 
391     @Test
testBitstreamsConformance()392     public void testBitstreamsConformance() throws Exception {
393         generateReportFile(".txt",
394                 MediaBitstreams.KEY_BITSTREAMS_VALIDATION_TXT,
395                 new TestBitstreamsConformance());
396     }
397 
398     /**
399      * Converts a single media track format string into a MediaFormat object
400      *
401      * @param trackFormatString a string representation of the format of one media track
402      * @return a MediaFormat
403      */
parseTrackFormat(String trackFormatString)404     private static MediaFormat parseTrackFormat(String trackFormatString) {
405         MediaFormat format = new MediaFormat();
406         format.setString(MediaFormat.KEY_MIME, "");
407         for (String entry : trackFormatString.split(",")) {
408             String[] kv = entry.split("=");
409             if (kv.length < 2 || kv[1].isEmpty()) {
410                 continue;
411             }
412             String k = kv[0];
413             String v = kv[1];
414             try {
415                 format.setInteger(k, Integer.parseInt(v));
416             } catch (NumberFormatException e) {
417                 format.setString(k, v);
418             }
419         }
420         return format;
421     }
422 }
423