blob: ddae30167004990c42db2f94dd9a4153b5b41b82 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.codec.cts;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import android.Manifest;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresDevice;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
// This class verifies the resource management aspects of MediaCodecs.
@Presubmit
@SmallTest
@RequiresDevice
public class MediaCodecResourceTest {
private static final String TAG = "MediaCodecResourceTest";
// Codec information that is pertinent to creating codecs for resource management testing.
private static class CodecInfo {
CodecInfo(String name, int maxSupportedInstances, String mime, MediaFormat mediaFormat) {
this.name = name;
this.maxSupportedInstances = maxSupportedInstances;
this.mime = mime;
this.mediaFormat = mediaFormat;
}
public final String name;
public final int maxSupportedInstances;
public final String mime;
public final MediaFormat mediaFormat;
}
// Resources are reclaimed from lower priority processes by higher priority processes.
private static int sLowPriorityPid = -1;
private static int sLowPriorityUid = -1;
private static int sHighPriorityPid = -1;
private static int sHighPriorityUid = -1;
@BeforeClass
public static void setup() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
Context context = instrumentation.getTargetContext();
//
// Start the low priority activity - set via the manifest to run in a different process.
//
{
Intent intent = new Intent(context, MediaCodecResourceTestLowPriorityActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
//
// Start the high priority activity - set via the manifest to run in a different process.
//
{
Intent intent = new Intent(context, MediaCodecResourceTestHighPriorityActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
//
// Scan all running processes to find the high and low processes (1 second grace period).
//
try {
// Permission needed to retrieve process information.
instrumentation.getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.REAL_GET_TASKS);
long startTimeMillis = System.currentTimeMillis();
while (sLowPriorityPid == -1 || sHighPriorityPid == -1) {
ActivityManager activityManager = context.getSystemService(ActivityManager.class);
for (RunningAppProcessInfo info : activityManager.getRunningAppProcesses()) {
if (info.processName.contains("MediaCodecResourceTestLowPriorityProcess")) {
sLowPriorityPid = info.pid;
sLowPriorityUid = info.uid;
}
if (info.processName.contains("MediaCodecResourceTestHighPriorityProcess")) {
sHighPriorityPid = info.pid;
sHighPriorityUid = info.uid;
}
}
// Starting activities takes a non-trivial amount of time, so allow 1 second.
// TODO(b/217746837): Synchronously start activities instead of timing out
if (System.currentTimeMillis() - startTimeMillis > 1000) {
throw new IllegalStateException("No low and high priority processes found.");
}
}
} finally {
instrumentation.getUiAutomation().dropShellPermissionIdentity();
}
}
@Test
public void testCreateCodecForAnotherProcessWithoutPermissionsThrows() throws Exception {
CodecInfo codecInfo = getFirstVideoHardwareCodec();
assumeTrue("No video hardware codec found.", codecInfo != null);
boolean wasSecurityExceptionThrown = false;
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
sLowPriorityPid, sLowPriorityUid);
fail("No SecurityException thrown when creating a codec for another process");
} catch (SecurityException ex) {
// expected
}
}
// A process with lower priority (e.g. background app) should not be able to reclaim
// MediaCodec resources from a process with higher priority (e.g. foreground app).
@Test
public void testLowerPriorityProcessFailsToReclaimResources() throws Exception {
CodecInfo codecInfo = getFirstVideoHardwareCodec();
assumeTrue("No video hardware codec found.", codecInfo != null);
assertTrue("Expected at least one max supported codec instance.",
codecInfo.maxSupportedInstances > 0);
List<MediaCodec> mediaCodecList = new ArrayList<>();
try {
// This permission is required to create MediaCodecs on behalf of other processes.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID);
// Create more codecs than are supported by the device on behalf of a high-priority
// process.
boolean wasInitialInsufficientResourcesExceptionThrown = false;
for (int i = 0; i <= codecInfo.maxSupportedInstances; ++i) {
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
sHighPriorityPid, sHighPriorityUid);
mediaCodecList.add(mediaCodec);
mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null,
/* crypto= */ null, /* flags= */ 0);
mediaCodec.start();
} catch (MediaCodec.CodecException ex) {
if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
wasInitialInsufficientResourcesExceptionThrown = true;
} else {
throw ex;
}
}
}
// For the same process, insufficient resources should be thrown.
assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
+ " resources after creating too many %d codecs for %s on behalf of the"
+ " same process", codecInfo.maxSupportedInstances, codecInfo.name),
wasInitialInsufficientResourcesExceptionThrown);
// Attempt to create the codec again, but this time, on behalf of a low priority
// process.
boolean wasLowPriorityInsufficientResourcesExceptionThrown = false;
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
sLowPriorityPid, sLowPriorityUid);
mediaCodecList.add(mediaCodec);
mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null, /* crypto= */ null,
/* flags= */ 0);
mediaCodec.start();
} catch (MediaCodec.CodecException ex) {
if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
wasLowPriorityInsufficientResourcesExceptionThrown = true;
} else {
throw ex;
}
}
assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
+ " resources after creating a follow-up codec for %s on behalf of a lower"
+ " priority process", codecInfo.mime),
wasLowPriorityInsufficientResourcesExceptionThrown);
} finally {
for (MediaCodec mediaCodec : mediaCodecList) {
mediaCodec.release();
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
// A process with higher priority (e.g. foreground app) should be able to reclaim
// MediaCodec resources from a process with lower priority (e.g. background app).
@Test
public void testHigherPriorityProcessReclaimsResources() throws Exception {
CodecInfo codecInfo = getFirstVideoHardwareCodec();
assumeTrue("No video hardware codec found.", codecInfo != null);
assertTrue("Expected at least one max supported codec instance.",
codecInfo.maxSupportedInstances > 0);
List<MediaCodec> mediaCodecList = new ArrayList<>();
try {
// This permission is required to create MediaCodecs on behalf of other processes.
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID);
// Create more codecs than are supported by the device on behalf of a low-priority
// process.
boolean wasInitialInsufficientResourcesExceptionThrown = false;
for (int i = 0; i <= codecInfo.maxSupportedInstances; ++i) {
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
sLowPriorityPid, sLowPriorityUid);
mediaCodecList.add(mediaCodec);
mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null,
/* crypto= */ null, /* flags= */ 0);
mediaCodec.start();
} catch (MediaCodec.CodecException ex) {
if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
wasInitialInsufficientResourcesExceptionThrown = true;
} else {
throw ex;
}
}
}
// For the same process, insufficient resources should be thrown.
assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
+ " resources after creating too many %d codecs for %s on behalf of the"
+ " same process", codecInfo.maxSupportedInstances, codecInfo.mime),
wasInitialInsufficientResourcesExceptionThrown);
// Attempt to create the codec again, but this time, on behalf of a high-priority
// process.
boolean wasHighPriorityInsufficientResourcesExceptionThrown = false;
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
sHighPriorityPid, sHighPriorityUid);
mediaCodecList.add(mediaCodec);
mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null, /* crypto= */ null,
/* flags= */ 0);
mediaCodec.start();
} catch (MediaCodec.CodecException ex) {
if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
wasHighPriorityInsufficientResourcesExceptionThrown = true;
} else {
throw ex;
}
}
assertFalse(String.format("Resource reclaiming should occur when creating a"
+ " follow-up codec for %s on behalf of a higher priority process, but"
+ " received an insufficient resource CodecException instead",
codecInfo.mime), wasHighPriorityInsufficientResourcesExceptionThrown);
} finally {
for (MediaCodec mediaCodec : mediaCodecList) {
mediaCodec.release();
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
// Find the first hardware video decoder and create a media format for it.
@Nullable
private CodecInfo getFirstVideoHardwareCodec() {
MediaCodecList allMediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo mediaCodecInfo : allMediaCodecList.getCodecInfos()) {
if (mediaCodecInfo.isSoftwareOnly()) {
continue;
}
if (mediaCodecInfo.isEncoder()) {
continue;
}
String mime = mediaCodecInfo.getSupportedTypes()[0];
CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime);
VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
if (videoCapabilities != null) {
int height = videoCapabilities.getSupportedHeights().getUpper();
int width = videoCapabilities.getSupportedWidthsFor(height).getUpper();
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, mime);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
return new CodecInfo(mediaCodecInfo.getName(),
codecCapabilities.getMaxSupportedInstances(), mime, mediaFormat);
}
}
return null;
}
}