/*
* Copyright 2020 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 androidx.camera.video.internal;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.camera.video.internal.compat.Api28Impl;
import androidx.camera.video.internal.compat.Api31Impl;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* Utility class for debugging.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class DebugUtils {
private static final String TAG = "DebugUtils";
private static final String CODEC_CAPS_PREFIX = "[CodecCaps] ";
private static final String VIDEO_CAPS_PREFIX = "[VideoCaps] ";
private static final String AUDIO_CAPS_PREFIX = "[AudioCaps] ";
private static final String ENCODER_CAPS_PREFIX = "[EncoderCaps] ";
private DebugUtils() {}
/**
* Returns a formatted string according to the input time, the format is
* "hours:minutes:seconds.milliseconds".
*
* @param time input time in microseconds.
* @return the formatted string.
*/
@NonNull
public static String readableUs(long time) {
return readableMs(TimeUnit.MICROSECONDS.toMillis(time));
}
/**
* Returns a formatted string according to the input time, the format is
* "hours:minutes:seconds.milliseconds".
*
* @param time input time in milliseconds.
* @return the formatted string.
*/
@NonNull
public static String readableMs(long time) {
return formatInterval(time);
}
/**
* Returns a formatted string according to the input {@link MediaCodec.BufferInfo}.
*
* @param bufferInfo the {@link MediaCodec.BufferInfo}.
* @return the formatted string.
*/
@NonNull
@SuppressWarnings("ObjectToString")
public static String readableBufferInfo(@NonNull MediaCodec.BufferInfo bufferInfo) {
StringBuilder sb = new StringBuilder();
sb.append("Dump BufferInfo: " + bufferInfo.toString() + "\n");
sb.append("\toffset: " + bufferInfo.offset + "\n");
sb.append("\tsize: " + bufferInfo.size + "\n");
{
sb.append("\tflag: " + bufferInfo.flags);
List<String> flagList = new ArrayList<>();
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
flagList.add("EOS");
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
flagList.add("CODEC_CONFIG");
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
flagList.add("KEY_FRAME");
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) != 0) {
flagList.add("PARTIAL_FRAME");
}
if (!flagList.isEmpty()) {
sb.append(" (").append(TextUtils.join(" | ", flagList)).append(")");
}
sb.append("\n");
}
sb.append("\tpresentationTime: " + bufferInfo.presentationTimeUs + " ("
+ readableUs(bufferInfo.presentationTimeUs) + ")\n");
return sb.toString();
}
private static String formatInterval(long millis) {
final long hr = TimeUnit.MILLISECONDS.toHours(millis);
final long min = TimeUnit.MILLISECONDS.toMinutes(millis - TimeUnit.HOURS.toMillis(hr));
final long sec = TimeUnit.MILLISECONDS.toSeconds(
millis - TimeUnit.HOURS.toMillis(hr) - TimeUnit.MINUTES.toMillis(min));
final long ms = millis - TimeUnit.HOURS.toMillis(hr) - TimeUnit.MINUTES.toMillis(min)
- TimeUnit.SECONDS.toMillis(sec);
return String.format(Locale.US, "%02d:%02d:%02d.%03d", hr, min, sec, ms);
}
/**
* Dumps {@link MediaCodecInfo} of input {@link MediaCodecList} and support for input
* {@link MediaFormat}.
*/
@NonNull
public static String dumpMediaCodecListForFormat(@NonNull MediaCodecList mediaCodecList,
@NonNull MediaFormat mediaFormat) {
StringBuilder sb = new StringBuilder();
logToString(sb, "[Start] Dump MediaCodecList for mediaFormat " + mediaFormat);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
for (MediaCodecInfo mediaCodecInfo : mediaCodecList.getCodecInfos()) {
if (!mediaCodecInfo.isEncoder()) {
continue;
}
try {
Preconditions.checkArgument(mime != null);
MediaCodecInfo.CodecCapabilities caps = mediaCodecInfo.getCapabilitiesForType(mime);
Preconditions.checkArgument(caps != null);
logToString(sb, "[Start] [" + mediaCodecInfo.getName() + "]");
dumpCodecCapabilities(sb, caps, mediaFormat);
logToString(sb, "[End] [" + mediaCodecInfo.getName() + "]");
} catch (IllegalArgumentException e) {
logToString(sb, "[" + mediaCodecInfo.getName() + "] does not support mime " + mime);
}
}
logToString(sb, "[End] Dump MediaCodecList");
String log = sb.toString();
stringToLog(log);
return log;
}
/**
* Dumps {@link MediaCodecInfo.CodecCapabilities} and {@link MediaFormat}.
*/
@NonNull
public static String dumpCodecCapabilities(@NonNull String mimeType, @NonNull MediaCodec codec,
@NonNull MediaFormat mediaFormat) {
StringBuilder sb = new StringBuilder();
try {
MediaCodecInfo.CodecCapabilities caps = codec.getCodecInfo().getCapabilitiesForType(
mimeType);
Preconditions.checkArgument(caps != null);
dumpCodecCapabilities(sb, caps, mediaFormat);
} catch (IllegalArgumentException e) {
logToString(sb, "[" + codec.getName() + "] does not support mime " + mimeType);
}
return sb.toString();
}
private static void dumpCodecCapabilities(@NonNull StringBuilder sb,
@NonNull MediaCodecInfo.CodecCapabilities caps,
@NonNull MediaFormat mediaFormat) {
try {
logToString(sb,
CODEC_CAPS_PREFIX + "isFormatSupported = " + caps.isFormatSupported(
mediaFormat));
} catch (ClassCastException e) {
logToString(sb, CODEC_CAPS_PREFIX + "isFormatSupported=false");
}
logToString(sb, CODEC_CAPS_PREFIX + "getDefaultFormat = " + caps.getDefaultFormat());
if (caps.profileLevels != null) {
StringBuilder stringBuilder = new StringBuilder("[");
List<String> profileLevelsStr = new ArrayList<>();
for (MediaCodecInfo.CodecProfileLevel profileLevel : caps.profileLevels) {
profileLevelsStr.add(toString(profileLevel));
}
stringBuilder.append(TextUtils.join(", ", profileLevelsStr)).append("]");
logToString(sb, CODEC_CAPS_PREFIX + "profileLevels = " + stringBuilder);
}
if (caps.colorFormats != null) {
logToString(sb,
CODEC_CAPS_PREFIX + "colorFormats = " + Arrays.toString(caps.colorFormats));
}
MediaCodecInfo.VideoCapabilities videoCaps = caps.getVideoCapabilities();
if (videoCaps != null) {
dumpVideoCapabilities(sb, videoCaps, mediaFormat);
}
MediaCodecInfo.AudioCapabilities audioCaps = caps.getAudioCapabilities();
if (audioCaps != null) {
dumpAudioCapabilities(sb, audioCaps, mediaFormat);
}
MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
if (encoderCaps != null) {
dumpEncoderCapabilities(sb, encoderCaps, mediaFormat);
}
}
private static void dumpVideoCapabilities(@NonNull StringBuilder sb,
@NonNull MediaCodecInfo.VideoCapabilities caps,
@NonNull MediaFormat mediaFormat) {
// Bitrate
logToString(sb, VIDEO_CAPS_PREFIX + "getBitrateRange = " + caps.getBitrateRange());
// Size
logToString(sb, VIDEO_CAPS_PREFIX + "getSupportedWidths = " + caps.getSupportedWidths()
+ ", getWidthAlignment = " + caps.getWidthAlignment());
logToString(sb, VIDEO_CAPS_PREFIX + "getSupportedHeights = " + caps.getSupportedHeights()
+ ", getHeightAlignment = " + caps.getHeightAlignment());
boolean hasSize = true;
int width;
int height;
try {
width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
Preconditions.checkArgument(width > 0 && height > 0);
} catch (NullPointerException | IllegalArgumentException e) {
logToString(sb,
VIDEO_CAPS_PREFIX + "mediaFormat does not contain valid width and height");
width = height = 0;
hasSize = false;
}
if (hasSize) {
try {
logToString(sb, VIDEO_CAPS_PREFIX + "getSupportedHeightsFor " + width + " = "
+ caps.getSupportedHeightsFor(width));
} catch (IllegalArgumentException e) {
logToString(sb, VIDEO_CAPS_PREFIX + "could not getSupportedHeightsFor " + width);
}
try {
logToString(sb, VIDEO_CAPS_PREFIX + "getSupportedWidthsFor " + height + " = "
+ caps.getSupportedWidthsFor(height));
} catch (IllegalArgumentException e) {
logToString(sb, VIDEO_CAPS_PREFIX + "could not getSupportedWidthsFor " + height);
}
logToString(sb, VIDEO_CAPS_PREFIX + "isSizeSupported for " + width + "x" + height
+ " = " + caps.isSizeSupported(width, height));
}
// Frame rate
logToString(sb,
VIDEO_CAPS_PREFIX + "getSupportedFrameRates = " + caps.getSupportedFrameRates());
int frameRate;
try {
frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
Preconditions.checkArgument(frameRate > 0);
} catch (NullPointerException | IllegalArgumentException e) {
logToString(sb, VIDEO_CAPS_PREFIX + "mediaFormat does not contain frame rate");
frameRate = 0;
}
if (hasSize) {
logToString(sb,
VIDEO_CAPS_PREFIX + "getSupportedFrameRatesFor " + width + "x" + height + " = "
+ caps.getSupportedFrameRatesFor(width, height));
}
if (hasSize && frameRate > 0) {
logToString(sb, VIDEO_CAPS_PREFIX + "areSizeAndRateSupported for "
+ width + "x" + height + ", " + frameRate
+ " = " + caps.areSizeAndRateSupported(width, height, frameRate));
}
}
private static void dumpAudioCapabilities(@NonNull StringBuilder sb,
@NonNull MediaCodecInfo.AudioCapabilities caps,
@NonNull MediaFormat mediaFormat) {
// Bitrate
logToString(sb, AUDIO_CAPS_PREFIX + "getBitrateRange = " + caps.getBitrateRange());
// Channel count
logToString(sb,
AUDIO_CAPS_PREFIX + "getMaxInputChannelCount = " + caps.getMaxInputChannelCount());
if (Build.VERSION.SDK_INT >= 31) {
logToString(sb, AUDIO_CAPS_PREFIX + "getMinInputChannelCount = "
+ Api31Impl.getMinInputChannelCount(caps));
logToString(sb, AUDIO_CAPS_PREFIX + "getInputChannelCountRanges = "
+ Arrays.toString(Api31Impl.getInputChannelCountRanges(caps)));
}
// Sample rate
logToString(sb, AUDIO_CAPS_PREFIX + "getSupportedSampleRateRanges = "
+ Arrays.toString(caps.getSupportedSampleRateRanges()));
logToString(sb, AUDIO_CAPS_PREFIX + "getSupportedSampleRates = "
+ Arrays.toString(caps.getSupportedSampleRates()));
try {
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
logToString(sb, AUDIO_CAPS_PREFIX + "isSampleRateSupported for " + sampleRate
+ " = " + caps.isSampleRateSupported(sampleRate));
} catch (NullPointerException | IllegalArgumentException e) {
logToString(sb, AUDIO_CAPS_PREFIX + "mediaFormat does not contain sample rate");
}
}
private static void dumpEncoderCapabilities(@NonNull StringBuilder sb,
@NonNull MediaCodecInfo.EncoderCapabilities caps,
@NonNull MediaFormat mediaFormat) {
logToString(sb, ENCODER_CAPS_PREFIX + "getComplexityRange = " + caps.getComplexityRange());
if (Build.VERSION.SDK_INT >= 28) {
logToString(sb,
ENCODER_CAPS_PREFIX + "getQualityRange = " + Api28Impl.getQualityRange(caps));
}
int bitrateMode;
try {
bitrateMode = mediaFormat.getInteger(MediaFormat.KEY_BITRATE_MODE);
logToString(sb,
ENCODER_CAPS_PREFIX + "isBitrateModeSupported = " + caps.isBitrateModeSupported(
bitrateMode));
} catch (NullPointerException | IllegalArgumentException e) {
logToString(sb, ENCODER_CAPS_PREFIX + "mediaFormat does not contain bitrate mode");
}
}
private static void logToString(@NonNull StringBuilder sb, @NonNull String message) {
sb.append(message);
sb.append("\n");
}
private static void stringToLog(@NonNull String log) {
if (Logger.isInfoEnabled(TAG)) {
Scanner scan = new Scanner(log);
while (scan.hasNextLine()) {
Logger.i(TAG, scan.nextLine());
}
}
}
@NonNull
private static String toString(@Nullable MediaCodecInfo.CodecProfileLevel codecProfileLevel) {
if (codecProfileLevel == null) {
return "null";
}
return String.format("{level=%d, profile=%d}", codecProfileLevel.level,
codecProfileLevel.profile);
}
}