blob: 18fe0b503a67c44b13ad510f5551ee5348b7bdfd [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 android.media.codec.cts.MediaCodecResourceTestHighPriorityActivity.ACTION_HIGH_PRIORITY_ACTIVITY_READY;
import static android.media.codec.cts.MediaCodecResourceTestLowPriorityService.ACTION_LOW_PRIORITY_SERVICE_READY;
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.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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 android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
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;
}
private static class ProcessInfo {
ProcessInfo(int pid, int uid) {
this.pid = pid;
this.uid = uid;
}
public final int pid;
public final int uid;
}
@Test
public void testCreateCodecForAnotherProcessWithoutPermissionsThrows() throws Exception {
CodecInfo codecInfo = getFirstVideoHardwareDecoder();
assumeTrue("No video hardware codec found.", codecInfo != null);
ProcessInfo processInfo = createLowPriorityProcess();
assertTrue("Unable to retrieve low priority process info.", processInfo != null);
boolean wasSecurityExceptionThrown = false;
try {
MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
processInfo.pid, processInfo.uid);
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 = getFirstVideoHardwareDecoder();
assumeTrue("No video hardware codec found.", codecInfo != null);
assertTrue("Expected at least one max supported codec instance.",
codecInfo.maxSupportedInstances > 0);
ProcessInfo lowPriorityProcess = createLowPriorityProcess();
assertTrue("Unable to retrieve low priority process info.", lowPriorityProcess != null);
ProcessInfo highPriorityProcess = createHighPriorityProcess();
assertTrue("Unable to retrieve high priority process info.", highPriorityProcess != null);
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);
Log.i(TAG, "Creating MediaCodecs on behalf of pid " + highPriorityProcess.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,
highPriorityProcess.pid, highPriorityProcess.uid);
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) {
Log.i(TAG, "Exception received on MediaCodec #" + i + ".");
wasInitialInsufficientResourcesExceptionThrown = true;
} else {
Log.e(TAG, "Unexpected exception thrown", ex);
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);
Log.i(TAG, "Creating MediaCodecs on behalf of pid " + lowPriorityProcess.pid);
// 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,
lowPriorityProcess.pid, lowPriorityProcess.uid);
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 {
Log.e(TAG, "Unexpected exception thrown", ex);
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 {
Log.i(TAG, "Cleaning up MediaCodecs");
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 = getFirstVideoHardwareDecoder();
assumeTrue("No video hardware codec found.", codecInfo != null);
assertTrue("Expected at least one max supported codec instance.",
codecInfo.maxSupportedInstances > 0);
ProcessInfo lowPriorityProcess = createLowPriorityProcess();
assertTrue("Unable to retrieve low priority process info.", lowPriorityProcess != null);
ProcessInfo highPriorityProcess = createHighPriorityProcess();
assertTrue("Unable to retrieve high priority process info.", highPriorityProcess != null);
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);
Log.i(TAG, "Creating MediaCodecs on behalf of pid " + lowPriorityProcess.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,
lowPriorityProcess.pid, lowPriorityProcess.uid);
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) {
Log.i(TAG, "Exception received on MediaCodec #" + i + ".");
wasInitialInsufficientResourcesExceptionThrown = true;
} else {
Log.e(TAG, "Unexpected exception thrown", ex);
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);
Log.i(TAG, "Creating final MediaCodec on behalf of pid " + highPriorityProcess.pid);
// 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,
highPriorityProcess.pid, highPriorityProcess.uid);
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 {
Log.e(TAG, "Unexpected exception thrown", ex);
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 {
Log.i(TAG, "Cleaning up MediaCodecs");
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 getFirstVideoHardwareDecoder() {
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().getLower();
int width = videoCapabilities.getSupportedWidthsFor(height).getLower();
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;
}
private ProcessInfo createLowPriorityProcess() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ProcessInfoBroadcastReceiver processInfoBroadcastReceiver =
new ProcessInfoBroadcastReceiver();
context.registerReceiver(processInfoBroadcastReceiver,
new IntentFilter(ACTION_LOW_PRIORITY_SERVICE_READY));
Intent intent = new Intent(context, MediaCodecResourceTestLowPriorityService.class);
context.startForegroundService(intent);
// Starting the service and receiving the broadcast should take less than 1 second
return processInfoBroadcastReceiver.waitForProcessInfoMs(1000);
}
private ProcessInfo createHighPriorityProcess() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ProcessInfoBroadcastReceiver processInfoBroadcastReceiver =
new ProcessInfoBroadcastReceiver();
context.registerReceiver(processInfoBroadcastReceiver,
new IntentFilter(ACTION_HIGH_PRIORITY_ACTIVITY_READY));
Intent intent = new Intent(context, MediaCodecResourceTestHighPriorityActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// Starting the activity and receiving the broadcast should take less than 1 second
return processInfoBroadcastReceiver.waitForProcessInfoMs(1000);
}
private static class ProcessInfoBroadcastReceiver extends BroadcastReceiver {
private int mPid = -1;
private int mUid = -1;
@Override
public void onReceive(Context context, Intent intent) {
synchronized (this) {
mPid = intent.getIntExtra("pid", -1);
mUid = intent.getIntExtra("uid", -1);
this.notify();
}
}
public ProcessInfo waitForProcessInfoMs(int milliseconds) {
synchronized (this) {
try {
this.wait(milliseconds);
} catch (InterruptedException ex) {
return null;
}
}
if (mPid == -1 || mUid == -1) {
return null;
}
return new ProcessInfo(mPid, mUid);
}
}
}