| /* |
| * Copyright 2018 Google Inc. All rights reserved. |
| * |
| * 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 static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF; |
| |
| import android.annotation.SuppressLint; |
| import android.graphics.Bitmap; |
| import android.media.MediaCodec; |
| import android.media.MediaFormat; |
| import android.media.MediaMuxer; |
| import android.os.Handler; |
| 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 java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteBuffer; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * This class writes one or more still images (of the same dimensions) into |
| * a heif file. |
| * |
| * It currently supports three input modes: {@link #INPUT_MODE_BUFFER}, |
| * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}. |
| * |
| * The general sequence (in pseudo-code) to write a heif file using this class is as follows: |
| * |
| * 1) Construct the writer: |
| * HeifWriter heifwriter = new HeifWriter(...); |
| * |
| * 2) If using surface input mode, obtain the input surface: |
| * Surface surface = heifwriter.getInputSurface(); |
| * |
| * 3) Call start: |
| * heifwriter.start(); |
| * |
| * 4) Depending on the chosen input mode, add one or more images using one of these methods: |
| * heifwriter.addYuvBuffer(...); Or |
| * heifwriter.addBitmap(...); Or |
| * render to the previously obtained surface |
| * |
| * 5) Call stop: |
| * heifwriter.stop(...); |
| * |
| * 6) Close the writer: |
| * heifwriter.close(); |
| * |
| * Please refer to the documentations on individual methods for the exact usage. |
| */ |
| public final class HeifWriter implements AutoCloseable { |
| private static final String TAG = "HeifWriter"; |
| private static final boolean DEBUG = false; |
| |
| private final @InputMode int mInputMode; |
| private final HandlerThread mHandlerThread; |
| private final Handler mHandler; |
| private int mNumTiles; |
| private final int mRotation; |
| private final int mMaxImages; |
| private final int mPrimaryIndex; |
| private final ResultWaiter mResultWaiter = new ResultWaiter(); |
| |
| private MediaMuxer mMuxer; |
| private HeifEncoder mHeifEncoder; |
| private int[] mTrackIndexArray; |
| private int mOutputIndex; |
| private boolean mStarted; |
| |
| /** |
| * The input mode where the client adds input buffers with YUV data. |
| * |
| * @see #addYuvBuffer(int, byte[]) |
| */ |
| public static final int INPUT_MODE_BUFFER = 0; |
| |
| /** |
| * The input mode where the client renders the images to an input Surface |
| * created by the writer. |
| * |
| * @see #getInputSurface() |
| */ |
| public static final int INPUT_MODE_SURFACE = 1; |
| |
| /** |
| * The input mode where the client adds bitmaps. |
| * |
| * @see #addBitmap(Bitmap) |
| */ |
| public static final int INPUT_MODE_BITMAP = 2; |
| |
| /** @hide */ |
| @IntDef({ |
| INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface InputMode {} |
| |
| /** |
| * Builder class for constructing a HeifWriter object from specified parameters. |
| */ |
| 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); |
| } |
| } |
| |
| @SuppressLint("WrongConstant") |
| 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) throws IOException { |
| if (primaryIndex >= maxImages) { |
| throw new IllegalArgumentException( |
| "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); |
| |
| // set to 1 initially, and wait for output format to know for sure |
| mNumTiles = 1; |
| |
| mRotation = rotation; |
| mInputMode = inputMode; |
| mMaxImages = maxImages; |
| mPrimaryIndex = primaryIndex; |
| |
| Looper looper = (handler != null) ? handler.getLooper() : null; |
| if (looper == null) { |
| mHandlerThread = new HandlerThread("HeifEncoderThread", |
| Process.THREAD_PRIORITY_FOREGROUND); |
| mHandlerThread.start(); |
| looper = mHandlerThread.getLooper(); |
| } else { |
| mHandlerThread = null; |
| } |
| mHandler = new Handler(looper); |
| |
| mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF) |
| : new MediaMuxer(fd, MUXER_OUTPUT_HEIF); |
| |
| mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality, |
| mInputMode, mHandler, new HeifCallback()); |
| } |
| |
| /** |
| * Start the heif writer. Can only be called once. |
| * |
| * @throws IllegalStateException if called more than once. |
| */ |
| public void start() { |
| checkStarted(false); |
| mStarted = true; |
| mHeifEncoder.start(); |
| } |
| |
| /** |
| * Add one YUV buffer to the heif file. |
| * |
| * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently |
| * only support YUV_420_888. |
| * |
| * @param data byte array containing the YUV data. If the format has more than one planes, |
| * they must be concatenated. |
| * |
| * @throws IllegalStateException if not started or not configured to use buffer input. |
| */ |
| public void addYuvBuffer(int format, @NonNull byte[] data) { |
| checkStartedAndMode(INPUT_MODE_BUFFER); |
| synchronized (this) { |
| if (mHeifEncoder != null) { |
| mHeifEncoder.addYuvBuffer(format, data); |
| } |
| } |
| } |
| |
| /** |
| * Retrieves the input surface for encoding. |
| * |
| * @return the input surface if configured to use surface input. |
| * |
| * @throws IllegalStateException if called after start or not configured to use surface input. |
| */ |
| public @NonNull Surface getInputSurface() { |
| checkStarted(false); |
| checkMode(INPUT_MODE_SURFACE); |
| return mHeifEncoder.getInputSurface(); |
| } |
| |
| /** |
| * Set the timestamp (in nano seconds) of the last input frame to encode. |
| * |
| * This call is only valid for surface input. Client can use this to stop the heif writer |
| * earlier before the maximum number of images are written. If not called, the writer will |
| * only stop when the maximum number of images are written. |
| * |
| * @param timestampNs timestamp (in nano seconds) of the last frame that will be written to the |
| * heif file. Frames with timestamps larger than the specified value will not |
| * be written. However, if a frame already started encoding when this is set, |
| * all tiles within that frame will be encoded. |
| * |
| * @throws IllegalStateException if not started or not configured to use surface input. |
| */ |
| public void setInputEndOfStreamTimestamp(long timestampNs) { |
| checkStartedAndMode(INPUT_MODE_SURFACE); |
| synchronized (this) { |
| if (mHeifEncoder != null) { |
| mHeifEncoder.setEndOfInputStreamTimestamp(timestampNs); |
| } |
| } |
| } |
| |
| /** |
| * Add one bitmap to the heif file. |
| * |
| * @param bitmap the bitmap to be added to the file. |
| * @throws IllegalStateException if not started or not configured to use bitmap input. |
| */ |
| public void addBitmap(@NonNull Bitmap bitmap) { |
| checkStartedAndMode(INPUT_MODE_BITMAP); |
| synchronized (this) { |
| if (mHeifEncoder != null) { |
| mHeifEncoder.addBitmap(bitmap); |
| } |
| } |
| } |
| |
| /** |
| * Stop the heif writer synchronously. Throws exception if the writer didn't finish writing |
| * successfully. Upon a success return: |
| * |
| * - For buffer and bitmap inputs, all images sent before stop will be written. |
| * |
| * - For surface input, images with timestamp on or before that specified in |
| * {@link #setInputEndOfStreamTimestamp(long)} will be written. In case where |
| * {@link #setInputEndOfStreamTimestamp(long)} was never called, stop will block |
| * until maximum number of images are received. |
| * |
| * @param timeoutMs Maximum time (in microsec) to wait for the writer to complete, with zero |
| * indicating waiting indefinitely. |
| * @see #setInputEndOfStreamTimestamp(long) |
| * @throws Exception if encountered error, in which case the output file may not be valid. In |
| * particular, {@link TimeoutException} is thrown when timed out, and {@link |
| * MediaCodec.CodecException} is thrown when encountered codec error. |
| */ |
| public void stop(long timeoutMs) throws Exception { |
| checkStarted(true); |
| synchronized (this) { |
| if (mHeifEncoder != null) { |
| mHeifEncoder.stopAsync(); |
| } |
| } |
| mResultWaiter.waitForResult(timeoutMs); |
| } |
| |
| private void checkStarted(boolean requiredStarted) { |
| if (mStarted != requiredStarted) { |
| throw new IllegalStateException("Already started"); |
| } |
| } |
| |
| private void checkMode(@InputMode int requiredMode) { |
| if (mInputMode != requiredMode) { |
| throw new IllegalStateException("Not valid in input mode " + mInputMode); |
| } |
| } |
| |
| private void checkStartedAndMode(@InputMode int requiredMode) { |
| checkStarted(true); |
| checkMode(requiredMode); |
| } |
| |
| /** |
| * Routine to stop and release writer, must be called on the same looper |
| * that receives heif encoder callbacks. |
| */ |
| private void closeInternal() { |
| if (DEBUG) Log.d(TAG, "closeInternal"); |
| |
| if (mMuxer != null) { |
| mMuxer.stop(); |
| mMuxer.release(); |
| mMuxer = null; |
| } |
| |
| if (mHeifEncoder != null) { |
| mHeifEncoder.close(); |
| synchronized (this) { |
| mHeifEncoder = null; |
| } |
| } |
| } |
| |
| /** |
| * Callback from the heif encoder. |
| */ |
| private class HeifCallback extends HeifEncoder.Callback { |
| /** |
| * Upon receiving output format from the encoder, add the requested number of |
| * image tracks to the muxer and start the muxer. |
| */ |
| @Override |
| public void onOutputFormatChanged( |
| @NonNull HeifEncoder encoder, @NonNull MediaFormat format) { |
| if (encoder != mHeifEncoder) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onOutputFormatChanged: " + format); |
| } |
| if (mTrackIndexArray != null) { |
| stopAndNotify(new IllegalStateException( |
| "Output format changed after muxer started")); |
| return; |
| } |
| |
| try { |
| int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS); |
| int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS); |
| mNumTiles = gridRows * gridCols; |
| } catch (NullPointerException | ClassCastException e) { |
| mNumTiles = 1; |
| } |
| |
| // 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 |
| format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0); |
| mTrackIndexArray[i] = mMuxer.addTrack(format); |
| } |
| mMuxer.start(); |
| } |
| |
| /** |
| * Upon receiving an output buffer from the encoder (which is one image when |
| * grid is not used, or one tile if grid is used), add that sample to the muxer. |
| */ |
| @Override |
| public void onDrainOutputBuffer( |
| @NonNull HeifEncoder encoder, @NonNull ByteBuffer byteBuffer) { |
| if (encoder != mHeifEncoder) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onDrainOutputBuffer: " + mOutputIndex); |
| } |
| if (mTrackIndexArray == null) { |
| stopAndNotify(new IllegalStateException( |
| "Output buffer received before format info")); |
| return; |
| } |
| |
| if (mOutputIndex < mMaxImages * mNumTiles) { |
| MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); |
| info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0); |
| mMuxer.writeSampleData( |
| mTrackIndexArray[mOutputIndex / mNumTiles], byteBuffer, info); |
| } |
| |
| mOutputIndex++; |
| |
| // post EOS if reached max number of images allowed. |
| if (mOutputIndex == mMaxImages * mNumTiles) { |
| stopAndNotify(null); |
| } |
| } |
| |
| @Override |
| public void onComplete(@NonNull HeifEncoder encoder) { |
| if (encoder != mHeifEncoder) return; |
| |
| stopAndNotify(null); |
| } |
| |
| @Override |
| public void onError(@NonNull HeifEncoder encoder, @NonNull MediaCodec.CodecException e) { |
| if (encoder != mHeifEncoder) return; |
| |
| stopAndNotify(e); |
| } |
| |
| private void stopAndNotify(@Nullable Exception error) { |
| try { |
| closeInternal(); |
| } catch (Exception e) { |
| // if there is an error during muxer stop, that must be propagated, |
| // unless error exists already. |
| if (error == null) { |
| error = e; |
| } |
| } |
| mResultWaiter.signalResult(error); |
| } |
| } |
| |
| private static class ResultWaiter { |
| private boolean mDone; |
| private Exception mException; |
| |
| synchronized void waitForResult(long timeoutMs) throws Exception { |
| if (timeoutMs < 0) { |
| throw new IllegalArgumentException("timeoutMs is negative"); |
| } |
| if (timeoutMs == 0) { |
| while (!mDone) { |
| try { |
| wait(); |
| } catch (InterruptedException ex) {} |
| } |
| } else { |
| final long startTimeMs = System.currentTimeMillis(); |
| long remainingWaitTimeMs = timeoutMs; |
| // avoid early termination by "spurious" wakeup. |
| while (!mDone && remainingWaitTimeMs > 0) { |
| try { |
| wait(remainingWaitTimeMs); |
| } catch (InterruptedException ex) {} |
| remainingWaitTimeMs -= (System.currentTimeMillis() - startTimeMs); |
| } |
| } |
| if (!mDone) { |
| mDone = true; |
| mException = new TimeoutException("timed out waiting for result"); |
| } |
| if (mException != null) { |
| throw mException; |
| } |
| } |
| |
| synchronized void signalResult(@Nullable Exception e) { |
| if (!mDone) { |
| mDone = true; |
| mException = e; |
| notifyAll(); |
| } |
| } |
| } |
| |
| @Override |
| public void close() { |
| mHandler.postAtFrontOfQueue(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| closeInternal(); |
| } catch (Exception e) { |
| // If the client called stop() properly, any errors would have been |
| // reported there. We don't want to crash when closing. |
| } |
| } |
| }); |
| } |
| } |