HeifWriter: add encoder preference configuration
After this change HeifWriter can take encoder preference (hw or sw
encoder, if CQ is enforced) as an additional check point for encoder
selection, and an exception will be thrown if no capable encoder is
found with the preference.
Bug: b/440162783
Test: ./gradlew heifwriter:heifwriter:connectedAndroidTest
Change-Id: I81efdc0f230a6c949926cca9a4bdf3d541dbd61f
diff --git a/heifwriter/heifwriter/api/current.txt b/heifwriter/heifwriter/api/current.txt
index f6e78d2..a933b0e 100644
--- a/heifwriter/heifwriter/api/current.txt
+++ b/heifwriter/heifwriter/api/current.txt
@@ -6,6 +6,7 @@
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public androidx.heifwriter.EncoderPreference getEncoderPreference();
method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
method public int getMaxImages();
@@ -26,6 +27,7 @@
ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.AvifWriter.Builder setEncoderPreference(androidx.heifwriter.EncoderPreference);
method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
@@ -35,11 +37,32 @@
method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
}
+ public class EncoderPreference {
+ method public int getBitrateMode();
+ method public static androidx.heifwriter.EncoderPreference getDefaultEncoderPreference();
+ method public int getEncoderType();
+ field public static final int CONSTANT_QUALITY_MODE_ONLY = 1; // 0x1
+ field public static final int CONSTANT_QUALITY_MODE_PREFERRED = 0; // 0x0
+ field public static final int HARDWARE_ENCODER_ONLY = 1; // 0x1
+ field public static final int HARDWARE_ENCODER_PREFERRED = 2; // 0x2
+ field public static final int NO_ENCODER_PREFERENCE = 0; // 0x0
+ field public static final int SOFTWARE_ENCODER_ONLY = 3; // 0x3
+ field public static final int SOFTWARE_ENCODER_PREFERRED = 4; // 0x4
+ }
+
+ public static final class EncoderPreference.Builder {
+ ctor public EncoderPreference.Builder();
+ method public androidx.heifwriter.EncoderPreference build();
+ method public androidx.heifwriter.EncoderPreference.Builder setBitrateMode(int);
+ method public androidx.heifwriter.EncoderPreference.Builder setEncoderType(int);
+ }
+
public final class HeifWriter implements java.lang.AutoCloseable {
method public void addBitmap(android.graphics.Bitmap);
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public androidx.heifwriter.EncoderPreference getEncoderPreference();
method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
method public int getMaxImages();
@@ -60,6 +83,7 @@
ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setEncoderPreference(androidx.heifwriter.EncoderPreference);
method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler?);
method public androidx.heifwriter.HeifWriter.Builder setMaxImages(@IntRange(from=1) int);
diff --git a/heifwriter/heifwriter/api/restricted_current.txt b/heifwriter/heifwriter/api/restricted_current.txt
index f6e78d2..a933b0e 100644
--- a/heifwriter/heifwriter/api/restricted_current.txt
+++ b/heifwriter/heifwriter/api/restricted_current.txt
@@ -6,6 +6,7 @@
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public androidx.heifwriter.EncoderPreference getEncoderPreference();
method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
method public int getMaxImages();
@@ -26,6 +27,7 @@
ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.AvifWriter.Builder setEncoderPreference(androidx.heifwriter.EncoderPreference);
method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
@@ -35,11 +37,32 @@
method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
}
+ public class EncoderPreference {
+ method public int getBitrateMode();
+ method public static androidx.heifwriter.EncoderPreference getDefaultEncoderPreference();
+ method public int getEncoderType();
+ field public static final int CONSTANT_QUALITY_MODE_ONLY = 1; // 0x1
+ field public static final int CONSTANT_QUALITY_MODE_PREFERRED = 0; // 0x0
+ field public static final int HARDWARE_ENCODER_ONLY = 1; // 0x1
+ field public static final int HARDWARE_ENCODER_PREFERRED = 2; // 0x2
+ field public static final int NO_ENCODER_PREFERENCE = 0; // 0x0
+ field public static final int SOFTWARE_ENCODER_ONLY = 3; // 0x3
+ field public static final int SOFTWARE_ENCODER_PREFERRED = 4; // 0x4
+ }
+
+ public static final class EncoderPreference.Builder {
+ ctor public EncoderPreference.Builder();
+ method public androidx.heifwriter.EncoderPreference build();
+ method public androidx.heifwriter.EncoderPreference.Builder setBitrateMode(int);
+ method public androidx.heifwriter.EncoderPreference.Builder setEncoderType(int);
+ }
+
public final class HeifWriter implements java.lang.AutoCloseable {
method public void addBitmap(android.graphics.Bitmap);
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public androidx.heifwriter.EncoderPreference getEncoderPreference();
method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
method public int getMaxImages();
@@ -60,6 +83,7 @@
ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setEncoderPreference(androidx.heifwriter.EncoderPreference);
method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler?);
method public androidx.heifwriter.HeifWriter.Builder setMaxImages(@IntRange(from=1) int);
diff --git a/heifwriter/heifwriter/build.gradle b/heifwriter/heifwriter/build.gradle
index 43f9d89..a50667e 100644
--- a/heifwriter/heifwriter/build.gradle
+++ b/heifwriter/heifwriter/build.gradle
@@ -14,7 +14,7 @@
android {
defaultConfig {
- minSdk = 28
+ minSdk = 29
}
namespace = "androidx.heifwriter"
}
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
index 2696941..574942d 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
@@ -17,8 +17,6 @@
package androidx.heifwriter;
import android.media.MediaCodec;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Handler;
import android.util.Log;
@@ -57,9 +55,6 @@
protected static final int ENCODING_BLOCK_SIZE = 64;
protected static final double MAX_COMPRESS_RATIO = 0.25f;
- private static final MediaCodecList sMCL =
- new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
/**
* Configure the avif encoding session. Should only be called once.
*
@@ -75,44 +70,15 @@
* @param cb The callback to receive various messages from the avif encoder.
*/
public AvifEncoder(int width, int height, boolean useGrid,
- int quality, @InputMode int inputMode,
+ int quality, @InputMode int inputMode, @NonNull EncoderPreference preference,
@Nullable Handler handler, @NonNull Callback cb,
boolean useBitDepth10) throws IOException {
- super("AVIF", width, height, useGrid, quality, inputMode, handler, cb, useBitDepth10);
+ super("AVIF", width, height, useGrid, quality, inputMode, preference, handler, cb,
+ useBitDepth10);
mEncoder.setCallback(new Av1EncoderCallback(), mHandler);
finishSettingUpEncoder(useBitDepth10);
}
- protected static String findAv1Fallback() {
- String av1 = null; // first AV1 encoder
- for (MediaCodecInfo info : sMCL.getCodecInfos()) {
- if (!info.isEncoder()) {
- continue;
- }
- MediaCodecInfo.CodecCapabilities caps = null;
- try {
- caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
- } catch (IllegalArgumentException e) { // mime is not supported
- continue;
- }
- if (!caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT)) {
- continue;
- }
- if (caps.getEncoderCapabilities().isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
- // Encoder that supports CQ mode is preferred over others,
- // return the first encoder that supports CQ mode.
- // (No need to check if it's hw based, it's already listed in
- // order of preference.)
- return info.getName();
- }
- if (av1 == null) {
- av1 = info.getName();
- }
- }
- // If no encoders support CQ, return the first AV1 encoder.
- return av1;
- }
/**
* MediaCodec callback for AV1 encoding.
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
index bb99957..866108b 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
@@ -128,6 +128,9 @@
private int mRotation = 0;
private Handler mHandler;
private boolean mHighBitDepthEnabled = false;
+ private EncoderPreference mEncoderPreference =
+ EncoderPreference.getDefaultEncoderPreference();
+
/**
* Construct a Builder with output specified by its path.
@@ -271,6 +274,21 @@
}
/**
+ * Sets the encoder preference for this builder.
+ *
+ * <p>This method allows you to configure the desired encoding type (hardware or software)
+ * and the bitrate mode (e.g., constant quality).
+ *
+ * @param preference The non-null {@link EncoderPreference} object used to specify
+ * the encoder's configuration.
+ * @return This {@code Builder} instance for method chaining.
+ */
+ public @NonNull Builder setEncoderPreference(@NonNull EncoderPreference preference) {
+ mEncoderPreference = preference;
+ return this;
+ }
+
+ /**
* Build a AvifWriter object.
*
* @return a AvifWriter object built according to the specifications.
@@ -279,7 +297,8 @@
*/
public @NonNull AvifWriter build() throws IOException {
return new AvifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
- mMaxImages, mPrimaryIndex, mInputMode, mHandler, mHighBitDepthEnabled);
+ mMaxImages, mPrimaryIndex, mInputMode, mEncoderPreference, mHandler,
+ mHighBitDepthEnabled);
}
}
@@ -391,7 +410,7 @@
@SuppressLint("WrongConstant")
@SuppressWarnings("WeakerAccess") /* synthetic access */
- AvifWriter(@NonNull String path,
+ AvifWriter(@NonNull String path,
@NonNull FileDescriptor fd,
int width,
int height,
@@ -401,9 +420,10 @@
int maxImages,
int primaryIndex,
@InputMode int inputMode,
+ @NonNull EncoderPreference preference,
@Nullable Handler handler,
boolean highBitDepthEnabled) throws IOException {
- super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+ super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality, preference,
handler, highBitDepthEnabled);
if (DEBUG) {
@@ -414,16 +434,17 @@
+ ", quality: " + quality
+ ", maxImages: " + maxImages
+ ", primaryIndex: " + primaryIndex
- + ", inputMode: " + inputMode);
+ + ", inputMode: " + inputMode
+ + ", encoder preference: " + preference);
}
+ mEncoder = new AvifEncoder(width, height, gridEnabled, quality,
+ mInputMode, preference, mHandler, new WriterCallback(), highBitDepthEnabled);
+
// set to 1 initially, and wait for output format to know for sure
mNumTiles = 1;
mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
: new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
-
- mEncoder = new AvifEncoder(width, height, gridEnabled, quality,
- mInputMode, mHandler, new WriterCallback(), highBitDepthEnabled);
}
}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
index 4f3210f..00e36ca 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
@@ -26,6 +26,7 @@
import android.media.MediaCodec.CodecException;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.opengl.GLES20;
import android.os.Handler;
@@ -74,11 +75,11 @@
private static final boolean DEBUG = false;
private String MIME;
- private int GRID_WIDTH;
- private int GRID_HEIGHT;
- private int ENCODING_BLOCK_SIZE;
- private double MAX_COMPRESS_RATIO;
- private int INPUT_BUFFER_POOL_SIZE = 2;
+ private static int GRID_WIDTH;
+ private static int GRID_HEIGHT;
+ private static int ENCODING_BLOCK_SIZE;
+ private static double MAX_COMPRESS_RATIO;
+ private static int INPUT_BUFFER_POOL_SIZE = 2;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaCodec mEncoder;
@@ -181,6 +182,73 @@
public abstract void onError(@NonNull EncoderBase encoder, @NonNull CodecException e);
}
+ private static @Nullable String findVideoEncoderFallback(@NonNull String mimeType,
+ @NonNull EncoderPreference preference) {
+ String encoder = null; // first encoder
+ String encoderHw = null; // first hardware encoder
+ String encoderSw = null; // first software encoder
+ final MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ boolean hwOk = (preference.getEncoderType() ==
+ EncoderPreference.HARDWARE_ENCODER_ONLY ||
+ preference.getEncoderType() == EncoderPreference.HARDWARE_ENCODER_PREFERRED ||
+ preference.getEncoderType() == EncoderPreference.NO_ENCODER_PREFERENCE);
+ boolean swOk = (preference.getEncoderType() ==
+ EncoderPreference.SOFTWARE_ENCODER_ONLY ||
+ preference.getEncoderType() == EncoderPreference.SOFTWARE_ENCODER_PREFERRED ||
+ preference.getEncoderType() == EncoderPreference.NO_ENCODER_PREFERENCE);
+
+ for (MediaCodecInfo info : list.getCodecInfos()) {
+ if (!info.isEncoder()) {
+ continue;
+ }
+
+ MediaCodecInfo.CodecCapabilities caps = null;
+ try {
+ caps = info.getCapabilitiesForType(mimeType);
+ } catch (IllegalArgumentException e) { // mime is not supported
+ continue;
+ }
+ if (!caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT)) {
+ continue;
+ }
+ boolean isHw = info.isHardwareAccelerated();
+ boolean isSw = !isHw;
+ if (isSw && preference.getEncoderType() ==
+ EncoderPreference.HARDWARE_ENCODER_ONLY) {
+ continue;
+ }
+ if (isHw && preference.getEncoderType() ==
+ EncoderPreference.SOFTWARE_ENCODER_ONLY) {
+ continue;
+ }
+ if (caps.getEncoderCapabilities().isBitrateModeSupported(
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+ // Encoder that supports CQ mode is preferred over others,
+ // return the first encoder that supports CQ mode if the
+ // hw/sw preference check is OK, cache otherwise
+ if ((isHw && hwOk) || (isSw && swOk)) {
+ return info.getName();
+ }
+ } else if (preference.getBitrateMode() ==
+ EncoderPreference.CONSTANT_QUALITY_MODE_ONLY) {
+ // Skip if CQ mode is enforced.
+ continue;
+ }
+ if (encoder == null) { encoder = info.getName(); }
+ if (isHw && encoderHw == null) { encoderHw = info.getName(); }
+ if (isSw && encoderSw == null) { encoderSw = info.getName(); }
+ }
+ // If no encoders support CQ, return the first encoder that meets the preference
+ // (nullable if no encoder meets the preference).
+ if (preference.getEncoderType() == EncoderPreference.HARDWARE_ENCODER_PREFERRED) {
+ return encoderHw != null ? encoderHw : encoder;
+ }
+ if (preference.getEncoderType() == EncoderPreference.SOFTWARE_ENCODER_PREFERRED) {
+ return encoderSw != null ? encoderSw : encoder;
+ }
+ return encoder;
+ }
+
/**
* Configure the encoder. Should only be called once.
*
@@ -197,7 +265,7 @@
* @param cb The callback to receive various messages from the heif encoder.
*/
protected EncoderBase(@NonNull String mimeType, int width, int height, boolean useGrid,
- int quality, @InputMode int inputMode,
+ int quality, @InputMode int inputMode, @NonNull EncoderPreference preference,
@Nullable Handler handler, @NonNull Callback cb,
boolean useBitDepth10) throws IOException {
if (DEBUG)
@@ -231,9 +299,18 @@
boolean useHeicEncoder = false;
MediaCodecInfo.CodecCapabilities caps = null;
+ String encoder = null;
switch (MIME) {
case "HEIC":
try {
+ // Skic HEIC encoder if software encoder is enforced
+ if (preference.getEncoderType() == EncoderPreference.SOFTWARE_ENCODER_ONLY
+ || preference.getEncoderType() ==
+ EncoderPreference.SOFTWARE_ENCODER_PREFERRED) {
+ mEncoder.release();
+ mEncoder = null;
+ throw new Exception();
+ }
mEncoder = MediaCodec.createEncoderByType(
MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
caps = mEncoder.getCodecInfo().getCapabilitiesForType(
@@ -246,7 +323,13 @@
}
useHeicEncoder = true;
} catch (Exception e) {
- mEncoder = MediaCodec.createByCodecName(HeifEncoder.findHevcFallback());
+ encoder = findVideoEncoderFallback(
+ MediaFormat.MIMETYPE_VIDEO_HEVC, preference);
+ if (encoder == null) {
+ throw new IllegalArgumentException("Cannot find capable "
+ + "encoder with the current encoder preference: " + preference);
+ }
+ mEncoder = MediaCodec.createByCodecName(encoder);
caps = mEncoder.getCodecInfo()
.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
// Disable grid if the image is too small
@@ -256,7 +339,13 @@
}
break;
case "AVIF":
- mEncoder = MediaCodec.createByCodecName(AvifEncoder.findAv1Fallback());
+ encoder = findVideoEncoderFallback(MediaFormat.MIMETYPE_VIDEO_AV1, preference);
+ if (encoder == null) {
+ throw new IllegalArgumentException("Cannot find capable"
+ + "encoder with the current encoder preference: " + preference);
+ }
+
+ mEncoder = MediaCodec.createByCodecName(encoder);
caps = mEncoder.getCodecInfo()
.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
// Disable grid if the image is too small
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderPreference.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderPreference.java
new file mode 100644
index 0000000..7a41db9
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderPreference.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2025 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.heifwriter;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Defines the configuration for an encoder, including hardware/software preference
+ * and constant quality (CQ) bitrate mode preference.
+ */
+public class EncoderPreference {
+ /**
+ * This is the default mode. System will use its default behavior.
+ */
+ public static final int NO_ENCODER_PREFERENCE = 0;
+
+ /**
+ * Only use a hardware encoder.
+ */
+ public static final int HARDWARE_ENCODER_ONLY = 1;
+
+ /**
+ * Prefer a hardware encoder, but fall back to a software encoder.
+ */
+ public static final int HARDWARE_ENCODER_PREFERRED = 2;
+ /**
+ * Only use a software encoder.
+ */
+ public static final int SOFTWARE_ENCODER_ONLY = 3;
+ /**
+ * Prefer a software encoder, but fall back to a hardware encoder.
+ */
+ public static final int SOFTWARE_ENCODER_PREFERRED = 4;
+
+ /**
+ * This is the default mode. Constant quality is not enforced,
+ * but encoders with constant quality support are prioritized.
+ */
+ public static final int CONSTANT_QUALITY_MODE_PREFERRED = 0;
+
+ /**
+ * Only choose the encoder that supports constant quality mode.
+ */
+ public static final int CONSTANT_QUALITY_MODE_ONLY = 1;
+
+ /**
+ * Representing the hardware or software preference for the encoder.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ NO_ENCODER_PREFERENCE,
+ HARDWARE_ENCODER_ONLY,
+ HARDWARE_ENCODER_PREFERRED,
+ SOFTWARE_ENCODER_ONLY,
+ SOFTWARE_ENCODER_PREFERRED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EncoderType {}
+
+ /**
+ * Representing the constant quality (CQ) bitrate mode preference.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ CONSTANT_QUALITY_MODE_PREFERRED, CONSTANT_QUALITY_MODE_ONLY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BitrateMode {}
+
+ private final @EncoderType int mEncodertype;
+ private final @BitrateMode int mBitrateMode;
+
+ /**
+ * Builder class for constructing a {@link EncoderPreference} object from specified parameters.
+ */
+ public static final class Builder {
+ private @EncoderType int mEncoderType = NO_ENCODER_PREFERENCE;
+ private @BitrateMode int mBitrateMode = CONSTANT_QUALITY_MODE_PREFERRED;
+
+ /**
+ * Creates a new Builder with default preference settings.
+ */
+ public Builder() {}
+
+ /**
+ * Sets the preferred encoding type.
+ *
+ * @param encoderType The preferred encoding type (HARDWARE or SOFTWARE).
+ * @return The Builder object to chain calls.
+ */
+ public @NonNull Builder setEncoderType(@EncoderType int encoderType) {
+ this.mEncoderType = encoderType;
+ return this;
+ }
+
+ /**
+ * Sets the preferred bitrate mode.
+ *
+ * @param bitrateMode The preferred bitrate mode.
+ * @return The Builder object to chain calls.
+ */
+ public @NonNull Builder setBitrateMode(@BitrateMode int bitrateMode) {
+ this.mBitrateMode = bitrateMode;
+ return this;
+ }
+
+ /**
+ * Creates an {@link EncoderPreference} object from the current settings.
+ *
+ * @return The immutable EncoderPreference object.
+ */
+ public @NonNull EncoderPreference build() {
+ return new EncoderPreference(mEncoderType, mBitrateMode);
+ }
+ }
+
+ /**
+ * Constructs an EncoderPreference object with the specified preferences.
+ *
+ * @param encodertype The preferred encoding type (HARDWARE or SOFTWARE).
+ * @param bitrateMode The preferred bitrate mode (CONSTANT_QUALITY, VARIABLE_BITRATE,
+ * or CONSTANT_BITRATE).
+ */
+ private EncoderPreference(@EncoderType int encodertype, @BitrateMode int bitrateMode) {
+ this.mEncodertype = encodertype;
+ this.mBitrateMode = bitrateMode;
+ }
+
+ /**
+ * Gets the default encoder preference.
+ *
+ * @return The EncoderPreference object.
+ */
+ public static @NonNull EncoderPreference getDefaultEncoderPreference() {
+ return new Builder().build();
+ }
+
+ /**
+ * Gets the preferred encoding type.
+ *
+ * @return The EncoderType.
+ */
+ public @EncoderType int getEncoderType() {
+ return mEncodertype;
+ }
+
+ /**
+ * Gets the preferred bitrate mode.
+ *
+ * @return The BitrateMode.
+ */
+ public @BitrateMode int getBitrateMode() {
+ return mBitrateMode;
+ }
+
+ @Override
+ public String toString() {
+ return "EncoderPreference: encodertype=" + getEncoderType() +
+ ", bitrateMode=" + getBitrateMode();
+ }
+}
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index abcfa0b..9a1c153 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -17,8 +17,6 @@
package androidx.heifwriter;
import android.media.MediaCodec;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Handler;
import android.util.Log;
@@ -53,9 +51,6 @@
protected static final int ENCODING_BLOCK_SIZE = 32;
protected static final double MAX_COMPRESS_RATIO = 0.25f;
- private static final MediaCodecList sMCL =
- new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
/**
* Configure the heif encoding session. Should only be called once.
*
@@ -71,45 +66,14 @@
* @param cb The callback to receive various messages from the heif encoder.
*/
public HeifEncoder(int width, int height, boolean useGrid,
- int quality, @InputMode int inputMode,
+ int quality, @InputMode int inputMode, @NonNull EncoderPreference preference,
@Nullable Handler handler, @NonNull Callback cb) throws IOException {
- super("HEIC", width, height, useGrid, quality, inputMode, handler, cb,
+ super("HEIC", width, height, useGrid, quality, inputMode, preference, handler, cb,
/* useBitDepth10 */ false);
mEncoder.setCallback(new HevcEncoderCallback(), mHandler);
finishSettingUpEncoder(/* useBitDepth10 */ false);
}
- protected static String findHevcFallback() {
- String hevc = null; // first HEVC encoder
- for (MediaCodecInfo info : sMCL.getCodecInfos()) {
- if (!info.isEncoder()) {
- continue;
- }
- MediaCodecInfo.CodecCapabilities caps = null;
- try {
- caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
- } catch (IllegalArgumentException e) { // mime is not supported
- continue;
- }
- if (!caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT)) {
- continue;
- }
- if (caps.getEncoderCapabilities().isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
- // Encoder that supports CQ mode is preferred over others,
- // return the first encoder that supports CQ mode.
- // (No need to check if it's hw based, it's already listed in
- // order of preference.)
- return info.getName();
- }
- if (hevc == null) {
- hevc = info.getName();
- }
- }
- // If no encoders support CQ, return the first HEVC encoder.
- return hevc;
- }
-
/**
* MediaCodec callback for HEVC encoding.
*/
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index 0518176..3df71de 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -125,6 +125,9 @@
private int mPrimaryIndex = 0;
private int mRotation = 0;
private Handler mHandler;
+ private EncoderPreference mEncoderPreference =
+ EncoderPreference.getDefaultEncoderPreference();
+
/**
* Construct a Builder with output specified by its path.
@@ -256,6 +259,21 @@
}
/**
+ * Sets the encoder preference for this builder.
+ *
+ * <p>This method allows you to configure the desired encoding type (hardware or software)
+ * and the bitrate mode (e.g., constant quality).
+ *
+ * @param preference The non-null {@link EncoderPreference} object used to specify
+ * the encoder's configuration.
+ * @return This {@code Builder} instance for method chaining.
+ */
+ public @NonNull Builder setEncoderPreference(@NonNull EncoderPreference preference) {
+ mEncoderPreference = preference;
+ return this;
+ }
+
+ /**
* Build a HeifWriter object.
*
* @return a HeifWriter object built according to the specifications.
@@ -264,7 +282,7 @@
*/
public @NonNull HeifWriter build() throws IOException {
return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
- mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+ mMaxImages, mPrimaryIndex, mInputMode, mEncoderPreference, mHandler);
}
}
@@ -376,7 +394,7 @@
@SuppressLint("WrongConstant")
@SuppressWarnings("WeakerAccess") /* synthetic access */
- HeifWriter(@NonNull String path,
+ HeifWriter(@NonNull String path,
@NonNull FileDescriptor fd,
int width,
int height,
@@ -386,8 +404,9 @@
int maxImages,
int primaryIndex,
@InputMode int inputMode,
+ @NonNull EncoderPreference preference,
@Nullable Handler handler) throws IOException {
- super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+ super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality, preference,
handler, /* highBitDepthEnabled */ false);
if (DEBUG) {
@@ -398,16 +417,17 @@
+ ", quality: " + quality
+ ", maxImages: " + maxImages
+ ", primaryIndex: " + primaryIndex
- + ", inputMode: " + inputMode);
+ + ", inputMode: " + inputMode
+ + ", encoder preference: " + preference);
}
+ mEncoder = new HeifEncoder(width, height, gridEnabled, quality,
+ mInputMode, preference, mHandler, new WriterCallback());
+
// set to 1 initially, and wait for output format to know for sure
mNumTiles = 1;
mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
: new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
-
- mEncoder = new HeifEncoder(width, height, gridEnabled, quality,
- mInputMode, mHandler, new WriterCallback());
}
}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
index d0bd2e0..bca9fe48 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
@@ -105,7 +105,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
final ResultWaiter mResultWaiter = new ResultWaiter();
@SuppressWarnings("WeakerAccess") /* synthetic access */
-protected @NonNull MediaMuxer mMuxer;
+ protected @NonNull MediaMuxer mMuxer;
protected @NonNull EncoderBase mEncoder;
final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
@SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -115,6 +115,8 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mGridEnabled;
@SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull EncoderPreference mEncoderPreference;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
int mQuality;
private boolean mStarted;
@@ -126,6 +128,7 @@
int primaryIndex,
boolean gridEnabled,
int quality,
+ @NonNull EncoderPreference preference,
@Nullable Handler handler,
boolean highBitDepthEnabled) throws IOException {
if (primaryIndex >= maxImages) {
@@ -140,6 +143,7 @@
mGridEnabled = gridEnabled;
mQuality = quality;
mHighBitDepthEnabled = highBitDepthEnabled;
+ mEncoderPreference = preference;
Looper looper = (handler != null) ? handler.getLooper() : null;
if (looper == null) {
@@ -569,4 +573,13 @@
public boolean isHighBitDepthEnabled() {
return mHighBitDepthEnabled;
}
-}
\ No newline at end of file
+
+ /**
+ * Gets the configured encoder preference.
+ *
+ * @return The {@link EncoderPreference} object containing the current encoding configuration.
+ */
+ public @NonNull EncoderPreference getEncoderPreference() {
+ return mEncoderPreference;
+ }
+}