heifwriter: rotation support -- DO NOT MERGE
Add rotation support, and change public constructor to
Builder instead so that additional parameters can be
supported later.
Test: HeifWriterTest; local test app with rotation flag set.
Bug: 63633199
Change-Id: I04d8a3fd8ab454f85c200b0e2e4c8ab8f5917d5a
(cherry picked from commit 941fb082e942763b49abf2a1be83ae418b7253dc)
diff --git a/heifwriter/api/current.txt b/heifwriter/api/current.txt
index c7311ba..3c39776 100644
--- a/heifwriter/api/current.txt
+++ b/heifwriter/api/current.txt
@@ -1,8 +1,6 @@
package androidx.heifwriter {
public final class HeifWriter implements java.lang.AutoCloseable {
- ctor public HeifWriter(java.lang.String, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
- ctor public HeifWriter(java.io.FileDescriptor, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
method public void addBitmap(android.graphics.Bitmap);
method public void addYuvBuffer(int, byte[]);
method public void close();
@@ -15,5 +13,17 @@
field public static final int INPUT_MODE_SURFACE = 1; // 0x1
}
+ public static final class HeifWriter.Builder {
+ ctor public HeifWriter.Builder(java.lang.String, int, int, int);
+ ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+ method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ 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(int);
+ method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(int);
+ method public androidx.heifwriter.HeifWriter.Builder setQuality(int);
+ method public androidx.heifwriter.HeifWriter.Builder setRotation(int);
+ }
+
}
diff --git a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index d5b339a..2527002 100644
--- a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -224,7 +224,11 @@
}
private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
- doTest(builder.setNumImages(4).build());
+ builder.setNumImages(4);
+ doTest(builder.setRotation(270).build());
+ doTest(builder.setRotation(180).build());
+ doTest(builder.setRotation(90).build());
+ doTest(builder.setRotation(0).build());
doTest(builder.setNumImages(1).build());
doTest(builder.setNumImages(8).build());
}
@@ -252,102 +256,109 @@
}
private static class TestConfig {
- final int inputMode;
- final boolean useGrid;
- final boolean useHandler;
- final int maxNumImages;
- final int numImages;
- final int width;
- final int height;
- final int quality;
- final String inputPath;
- final String outputPath;
- final Bitmap[] bitmaps;
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ final int mMaxNumImages;
+ final int mNumImages;
+ final int mWidth;
+ final int mHeight;
+ final int mRotation;
+ final int mQuality;
+ final String mInputPath;
+ final String mOutputPath;
+ final Bitmap[] mBitmaps;
- TestConfig(int _inputMode, boolean _useGrid, boolean _useHandler,
- int _maxNumImage, int _numImages, int _width, int _height, int _quality,
- String _inputPath, String _outputPath, Bitmap[] _bitmaps) {
- inputMode = _inputMode;
- useGrid = _useGrid;
- useHandler = _useHandler;
- maxNumImages = _maxNumImage;
- numImages = _numImages;
- width = _width;
- height = _height;
- quality = _quality;
- inputPath = _inputPath;
- outputPath = _outputPath;
- bitmaps = _bitmaps;
+ TestConfig(int inputMode, boolean useGrid, boolean useHandler,
+ int maxNumImages, int numImages, int width, int height,
+ int rotation, int quality,
+ String inputPath, String outputPath, Bitmap[] bitmaps) {
+ mInputMode = inputMode;
+ mUseGrid = useGrid;
+ mUseHandler = useHandler;
+ mMaxNumImages = maxNumImages;
+ mNumImages = numImages;
+ mWidth = width;
+ mHeight = height;
+ mRotation = rotation;
+ mQuality = quality;
+ mInputPath = inputPath;
+ mOutputPath = outputPath;
+ mBitmaps = bitmaps;
}
static class Builder {
- final int inputMode;
- final boolean useGrid;
- final boolean useHandler;
- int maxNumImages;
- int numImages;
- int width;
- int height;
- int quality;
- String inputPath;
- final String outputPath;
- Bitmap[] bitmaps;
-
- boolean numImagesSetExplicitly;
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ int mMaxNumImages;
+ int mNumImages;
+ int mWidth;
+ int mHeight;
+ int mRotation;
+ final int mQuality;
+ String mInputPath;
+ final String mOutputPath;
+ Bitmap[] mBitmaps;
+ boolean mNumImagesSetExplicitly;
- Builder(int _inputMode, boolean _useGrids, boolean _useHandler) {
- inputMode = _inputMode;
- useGrid = _useGrids;
- useHandler = _useHandler;
- maxNumImages = numImages = 4;
- width = 1920;
- height = 1080;
- quality = 100;
- outputPath = new File(Environment.getExternalStorageDirectory(),
+ Builder(int inputMode, boolean useGrids, boolean useHandler) {
+ mInputMode = inputMode;
+ mUseGrid = useGrids;
+ mUseHandler = useHandler;
+ mMaxNumImages = mNumImages = 4;
+ mWidth = 1920;
+ mHeight = 1080;
+ mRotation = 0;
+ mQuality = 100;
+ mOutputPath = new File(Environment.getExternalStorageDirectory(),
OUTPUT_FILENAME).getAbsolutePath();
}
- Builder setInputPath(String _inputPath) {
- inputPath = (inputMode == INPUT_MODE_BITMAP) ? _inputPath : null;
+ Builder setInputPath(String inputPath) {
+ mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
return this;
}
- Builder setNumImages(int _numImages) {
- numImagesSetExplicitly = true;
- numImages = _numImages;
+ Builder setNumImages(int numImages) {
+ mNumImagesSetExplicitly = true;
+ mNumImages = numImages;
+ return this;
+ }
+
+ Builder setRotation(int rotation) {
+ mRotation = rotation;
return this;
}
private void loadBitmapInputs() {
- if (inputMode != INPUT_MODE_BITMAP) {
+ if (mInputMode != INPUT_MODE_BITMAP) {
return;
}
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- retriever.setDataSource(inputPath);
+ retriever.setDataSource(mInputPath);
String hasImage = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
if (!"yes".equals(hasImage)) {
throw new IllegalArgumentException("no bitmap found!");
}
- width = Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH));
- height = Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT));
- maxNumImages = Math.min(maxNumImages, Integer.parseInt(retriever.extractMetadata(
+ mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
- if (!numImagesSetExplicitly) {
- numImages = maxNumImages;
+ if (!mNumImagesSetExplicitly) {
+ mNumImages = mMaxNumImages;
}
- bitmaps = new Bitmap[maxNumImages];
- for (int i = 0; i < bitmaps.length; i++) {
- bitmaps[i] = retriever.getImageAtIndex(i);
+ mBitmaps = new Bitmap[mMaxNumImages];
+ for (int i = 0; i < mBitmaps.length; i++) {
+ mBitmaps[i] = retriever.getImageAtIndex(i);
}
+ mWidth = mBitmaps[0].getWidth();
+ mHeight = mBitmaps[0].getHeight();
retriever.release();
}
private void cleanupStaleOutputs() {
- File outputFile = new File(outputPath);
+ File outputFile = new File(mOutputPath);
if (outputFile.exists()) {
outputFile.delete();
}
@@ -357,58 +368,61 @@
cleanupStaleOutputs();
loadBitmapInputs();
- return new TestConfig(inputMode, useGrid, useHandler, maxNumImages, numImages,
- width, height, quality, inputPath, outputPath, bitmaps);
+ return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
+ mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
}
}
@Override
public String toString() {
- return "TestConfig" +
- ": inputMode " + inputMode +
- ", useGrid " + useGrid +
- ", useHandler " + useHandler +
- ", maxNumImages " + maxNumImages +
- ", numImages " + numImages +
- ", width " + width +
- ", height " + height +
- ", quality " + quality +
- ", inputPath " + inputPath +
- ", outputPath " + outputPath;
+ return "TestConfig"
+ + ": mInputMode " + mInputMode
+ + ", mUseGrid " + mUseGrid
+ + ", mUseHandler " + mUseHandler
+ + ", mMaxNumImages " + mMaxNumImages
+ + ", mNumImages " + mNumImages
+ + ", mWidth " + mWidth
+ + ", mHeight " + mHeight
+ + ", mRotation " + mRotation
+ + ", mQuality " + mQuality
+ + ", mInputPath " + mInputPath
+ + ", mOutputPath " + mOutputPath;
}
}
- private void doTest(TestConfig testConfig) throws Exception {
- int width = testConfig.width;
- int height = testConfig.height;
- int numImages = testConfig.numImages;
+ private void doTest(TestConfig config) throws Exception {
+ int width = config.mWidth;
+ int height = config.mHeight;
+ int numImages = config.mNumImages;
mInputIndex = 0;
HeifWriter heifWriter = null;
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
- if (DEBUG) Log.d(TAG, "started: " + testConfig);
+ if (DEBUG) Log.d(TAG, "started: " + config);
- heifWriter = new HeifWriter(testConfig.outputPath, width, height,
- testConfig.useGrid,
- testConfig.quality,
- testConfig.maxNumImages,
- testConfig.maxNumImages - 1,
- testConfig.inputMode,
- testConfig.useHandler ? mHandler : null);
+ heifWriter = new HeifWriter.Builder(
+ config.mOutputPath, width, height, config.mInputMode)
+ .setRotation(config.mRotation)
+ .setGridEnabled(config.mUseGrid)
+ .setMaxImages(config.mMaxNumImages)
+ .setQuality(config.mQuality)
+ .setPrimaryIndex(config.mMaxNumImages - 1)
+ .setHandler(config.mUseHandler ? mHandler : null)
+ .build();
- if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+ if (config.mInputMode == INPUT_MODE_SURFACE) {
mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
}
heifWriter.start();
- if (testConfig.inputMode == INPUT_MODE_BUFFER) {
+ if (config.mInputMode == INPUT_MODE_BUFFER) {
byte[] data = new byte[width * height * 3 / 2];
- if (testConfig.inputPath != null) {
- inputStream = new FileInputStream(testConfig.inputPath);
+ if (config.mInputPath != null) {
+ inputStream = new FileInputStream(config.mInputPath);
}
if (DUMP_YUV_INPUT) {
@@ -426,7 +440,7 @@
}
heifWriter.addYuvBuffer(ImageFormat.YUV_420_888, data);
}
- } else if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+ } else if (config.mInputMode == INPUT_MODE_SURFACE) {
// The input surface is a surface texture using single buffer mode, draws will be
// blocked until onFrameAvailable is done with the buffer, which is dependant on
// how fast MediaCodec processes them, which is further dependent on how fast the
@@ -438,8 +452,8 @@
}
heifWriter.setInputEndOfStreamTimestamp(
1000 * computePresentationTime(numImages - 1));
- } else if (testConfig.inputMode == INPUT_MODE_BITMAP) {
- Bitmap[] bitmaps = testConfig.bitmaps;
+ } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+ Bitmap[] bitmaps = config.mBitmaps;
for (int i = 0; i < Math.min(bitmaps.length, numImages); i++) {
if (DEBUG) Log.d(TAG, "addBitmap: " + i);
heifWriter.addBitmap(bitmaps[i]);
@@ -448,8 +462,8 @@
}
heifWriter.stop(3000);
- verifyResult(testConfig.outputPath, width, height, testConfig.useGrid,
- Math.min(numImages, testConfig.maxNumImages));
+ verifyResult(config.mOutputPath, width, height, config.mRotation, config.mUseGrid,
+ Math.min(numImages, config.mMaxNumImages));
if (DEBUG) Log.d(TAG, "finished: PASS");
} finally {
try {
@@ -532,7 +546,7 @@
}
private void verifyResult(
- String filename, int width, int height, boolean useGrid, int numImages)
+ String filename, int width, int height, int rotation, boolean useGrid, int numImages)
throws Exception {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(filename);
@@ -542,13 +556,16 @@
}
assertEquals("Wrong image count", numImages,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
assertEquals("Wrong width", width,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
assertEquals("Wrong height", height,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+ assertEquals("Wrong rotation", rotation,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
retriever.release();
if (useGrid) {
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 3020b96..40b1835 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -213,7 +213,14 @@
int gridWidth, gridHeight, gridRows, gridCols;
- useGrid = useGrid && (width > GRID_WIDTH || height > GRID_HEIGHT);
+ MediaCodecInfo.CodecCapabilities caps =
+ mEncoder.getCodecInfo().getCapabilitiesForType(useHeicEncoder
+ ? MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC
+ : MediaFormat.MIMETYPE_VIDEO_HEVC);
+
+ useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+ // Always enable grid if the size is too large for the HEVC encoder
+ useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
if (useGrid) {
gridWidth = GRID_WIDTH;
@@ -258,15 +265,11 @@
codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-
- MediaCodecInfo.CodecCapabilities caps =
- mEncoder.getCodecInfo().getCapabilitiesForType(useHeicEncoder
- ? MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC
- : MediaFormat.MIMETYPE_VIDEO_HEVC);
- MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
codecFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, mNumTiles * 30);
+
+ MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
if (encoderCaps.isBitrateModeSupported(
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
Log.d(TAG, "Setting bitrate mode to constant quality");
@@ -278,14 +281,14 @@
(qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
} else {
if (encoderCaps.isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)) {
- Log.d(TAG, "Setting bitrate mode to variable bitrate");
- codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
- } else { // assume CBR
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
Log.d(TAG, "Setting bitrate mode to constant bitrate");
codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+ } else { // assume VBR
+ Log.d(TAG, "Setting bitrate mode to variable bitrate");
+ codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
}
// Calculate the bitrate based on image dimension, max compression ratio and quality.
// Note that we set the frame rate to the number of tiles, so the bitrate would be the
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index be7dffb..fff1a9b 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -16,6 +16,8 @@
package androidx.heifwriter;
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.media.MediaCodec;
@@ -25,11 +27,12 @@
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
+import android.util.Log;
+import android.view.Surface;
+
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import android.util.Log;
-import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -38,8 +41,6 @@
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
-import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
-
/**
* This class writes one or more still images (of the same dimensions) into
* a heif file.
@@ -79,7 +80,8 @@
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private int mNumTiles;
- private final int mNumImages;
+ private final int mRotation;
+ private final int mMaxImages;
private final int mPrimaryIndex;
private final ResultWaiter mResultWaiter = new ResultWaiter();
@@ -119,96 +121,200 @@
public @interface InputMode {}
/**
- * Construct a heif writer that writes to a file specified by its path.
- *
- * @param path Path of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
- * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
- * automatically chosen.
- * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
- * supported by this implementation (which often results in larger file size).
- * @param numImages Max number of images to write. Frames exceeding this number will not be
- * written to file. The writing can be stopped earlier before this number of
- * images are written by {@link #stop(long)}, except for the input mode of
- * {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
- * {@link #setInputEndOfStreamTimestamp(long)} and reached.
- * @param primaryIndex Index of the image that should be marked as primary, must be within range
- * [0, numImages - 1] inclusive.
- * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
- * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
- * @param handler If not null, client will receive all callbacks on the handler's looper.
- * Otherwise, client will receive callbacks on a looper created by the writer.
- *
- * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
+ * Builder class for constructing a HeifWriter object from specified parameters.
*/
- @SuppressLint("WrongConstant")
- public HeifWriter(@NonNull String path,
- int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
- @InputMode int inputMode,
- @Nullable Handler handler) throws IOException {
- this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
- new MediaMuxer(path, MUXER_OUTPUT_HEIF));
+ public static final class Builder {
+ private final String mPath;
+ private final FileDescriptor mFd;
+ private final int mWidth;
+ private final int mHeight;
+ private final @InputMode int mInputMode;
+ private boolean mGridEnabled = true;
+ private int mQuality = 100;
+ private int mMaxImages = 1;
+ private int mPrimaryIndex = 0;
+ private int mRotation = 0;
+ private Handler mHandler;
+
+ /**
+ * Construct a Builder with output specified by its path.
+ *
+ * @param path Path of the file to be written.
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull String path,
+ int width, int height, @InputMode int inputMode) {
+ this(path, null, width, height, inputMode);
+ }
+
+ /**
+ * Construct a Builder with output specified by its file descriptor.
+ *
+ * @param fd File descriptor of the file to be written.
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull FileDescriptor fd,
+ int width, int height, @InputMode int inputMode) {
+ this(null, fd, width, height, inputMode);
+ }
+
+ private Builder(String path, FileDescriptor fd,
+ int width, int height, @InputMode int inputMode) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
+ }
+ mPath = path;
+ mFd = fd;
+ mWidth = width;
+ mHeight = height;
+ mInputMode = inputMode;
+ }
+
+ /**
+ * Set the image rotation in degrees.
+ *
+ * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
+ * Default is 0.
+ * @return this Builder object.
+ */
+ public Builder setRotation(int rotation) {
+ if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+ throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+ }
+ mRotation = rotation;
+ return this;
+ }
+
+ /**
+ * Set whether to enable grid option.
+ *
+ * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+ * automatically chosen. Default is to enable.
+ * @return this Builder object.
+ */
+ public Builder setGridEnabled(boolean gridEnabled) {
+ mGridEnabled = gridEnabled;
+ return this;
+ }
+
+ /**
+ * Set the quality for encoding images.
+ *
+ * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+ * quality supported by this implementation. Default is 100.
+ * @return this Builder object.
+ */
+ public Builder setQuality(int quality) {
+ if (quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("Invalid quality: " + quality);
+ }
+ mQuality = quality;
+ return this;
+ }
+
+ /**
+ * Set the maximum number of images to write.
+ *
+ * @param maxImages Max number of images to write. Frames exceeding this number will not be
+ * written to file. The writing can be stopped earlier before this number
+ * of images are written by {@link #stop(long)}, except for the input mode
+ * of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+ * specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+ * Default is 1.
+ * @return this Builder object.
+ */
+ public Builder setMaxImages(int maxImages) {
+ if (maxImages <= 0) {
+ throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+ }
+ mMaxImages = maxImages;
+ return this;
+ }
+
+ /**
+ * Set the primary image index.
+ *
+ * @param primaryIndex Index of the image that should be marked as primary, must be within
+ * range [0, maxImages - 1] inclusive. Default is 0.
+ * @return this Builder object.
+ */
+ public Builder setPrimaryIndex(int primaryIndex) {
+ if (primaryIndex < 0) {
+ throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
+ }
+ mPrimaryIndex = primaryIndex;
+ return this;
+ }
+
+ /**
+ * Provide a handler for the HeifWriter to use.
+ *
+ * @param handler If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on a looper created by the
+ * writer. Default is null.
+ * @return this Builder object.
+ */
+ public Builder setHandler(@Nullable Handler handler) {
+ mHandler = handler;
+ return this;
+ }
+
+ /**
+ * Build a HeifWriter object.
+ *
+ * @return a HeifWriter object built according to the specifications.
+ * @throws IOException if failed to create the writer, possibly due to failure to create
+ * {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+ */
+ public HeifWriter build() throws IOException {
+ return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+ mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+ }
}
- /**
- * Construct a heif writer that writes to a file specified by file descriptor.
- *
- * @param fd File descriptor of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
- * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
- * automatically chosen.
- * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
- * supported by this implementation (which often results in larger file size).
- * @param numImages Max number of images to write. Frames exceeding this number will not be
- * written to file. The writing can be stopped earlier before this number of
- * images are written by {@link #stop(long)}, except for the input mode of
- * {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
- * {@link #setInputEndOfStreamTimestamp(long)} and reached.
- * @param primaryIndex Index of the image that should be marked as primary, must be within range
- * [0, numImages - 1] inclusive.
- * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
- * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
- * @param handler If not null, client will receive all callbacks on the handler's looper.
- * Otherwise, client will receive callbacks on a looper created by the writer.
- *
- * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
- */
@SuppressLint("WrongConstant")
- public HeifWriter(@NonNull FileDescriptor fd,
- int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
- @InputMode int inputMode,
- @Nullable Handler handler) throws IOException {
- this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
- new MediaMuxer(fd, MUXER_OUTPUT_HEIF));
- }
-
- private HeifWriter(int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
+ private HeifWriter(@NonNull String path,
+ @NonNull FileDescriptor fd,
+ int width,
+ int height,
+ int rotation,
+ boolean gridEnabled,
+ int quality,
+ int maxImages,
+ int primaryIndex,
@InputMode int inputMode,
- @Nullable Handler handler,
- @NonNull MediaMuxer muxer) throws IOException {
- if (numImages <= 0 || primaryIndex < 0 || primaryIndex >= numImages) {
+ @Nullable Handler handler) throws IOException {
+ if (primaryIndex >= maxImages) {
throw new IllegalArgumentException(
- "Invalid numImages (" + numImages + ") or primaryIndex (" + primaryIndex + ")");
+ "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "width: " + width
+ + ", height: " + height
+ + ", rotation: " + rotation
+ + ", gridEnabled: " + gridEnabled
+ + ", quality: " + quality
+ + ", maxImages: " + maxImages
+ + ", primaryIndex: " + primaryIndex
+ + ", inputMode: " + inputMode);
}
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, width, height);
- if (DEBUG) {
- Log.d(TAG, "format: " + format + ", inputMode: " + inputMode +
- ", numImage: " + numImages + ", primaryIndex: " + primaryIndex);
- }
-
// set to 1 initially, and wait for output format to know for sure
mNumTiles = 1;
+ mRotation = rotation;
mInputMode = inputMode;
- mNumImages = numImages;
+ mMaxImages = maxImages;
mPrimaryIndex = primaryIndex;
Looper looper = (handler != null) ? handler.getLooper() : null;
@@ -222,9 +328,10 @@
}
mHandler = new Handler(looper);
- mMuxer = muxer;
+ mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+ : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
- mHeifEncoder = new HeifEncoder(width, height, useGrid, quality,
+ mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
mInputMode, mHandler, new HeifCallback());
}
@@ -406,13 +513,17 @@
mNumTiles = 1;
}
- // add mNumImages image tracks of the same format
- mTrackIndexArray = new int[mNumImages];
+ // add mMaxImages image tracks of the same format
+ mTrackIndexArray = new int[mMaxImages];
+
+ // set rotation angle
+ if (mRotation > 0) {
+ Log.d(TAG, "setting rotation: " + mRotation);
+ mMuxer.setOrientationHint(mRotation);
+ }
for (int i = 0; i < mTrackIndexArray.length; i++) {
// mark primary
- if (i == mPrimaryIndex) {
- format.setInteger(MediaFormat.KEY_IS_DEFAULT, 1);
- }
+ format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
mTrackIndexArray[i] = mMuxer.addTrack(format);
}
mMuxer.start();
@@ -436,7 +547,7 @@
return;
}
- if (mOutputIndex < mNumImages * mNumTiles) {
+ if (mOutputIndex < mMaxImages * mNumTiles) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
mMuxer.writeSampleData(
@@ -446,7 +557,7 @@
mOutputIndex++;
// post EOS if reached max number of images allowed.
- if (mOutputIndex == mNumImages * mNumTiles) {
+ if (mOutputIndex == mMaxImages * mNumTiles) {
stopAndNotify(null);
}
}