HeifWriter.java

/*
 * 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.util.Pair;
import android.view.Surface;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

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.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Writes one or more still images (of the same dimensions) into
 * a heif file.
 *
 * <p>This class currently supports three input modes: {@link #INPUT_MODE_BUFFER},
 * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
 *
 * <p>The general sequence (in pseudo-code) to write a heif file using this class is as follows:
 *
 * <p>1) Construct the writer:<br>
 * <code>HeifWriter heifwriter = new HeifWriter(...);</code>
 *
 * <p>2) If using surface input mode, obtain the input surface:<br>
 * <code>Surface surface = heifwriter.getInputSurface();</code>
 *
 * <p>3) Call <code>start</code>:<br>
 * <code>heifwriter.start();</code>
 *
 * <p>4) Depending on the chosen input mode, add one or more images using one of these methods:<br>
 * <code>heifwriter.addYuvBuffer(...);</code>   Or<br>
 * <code>heifwriter.addBitmap(...);</code>   Or<br>
 *
 * render to the previously obtained surface
 *
 * <p>5) Call <code>stop</code>:<br>
 * <code>heifwriter.stop(...);</code>
 *
 * <p>6) Close the writer:<br>
 * <code>heifwriter.close();</code>
 *
 * <p>Please refer to the documentations on individual methods for the exact usage.
 */
@SuppressWarnings("HiddenSuperclass")
public final class HeifWriter extends WriterBase {
    private static final String TAG = "HeifWriter";
    private static final boolean DEBUG = false;

    /**
     * The input mode where the client adds input buffers with YUV data.
     *
     * @see #addYuvBuffer(int, byte[])
     */
    public static final int INPUT_MODE_BUFFER = WriterBase.INPUT_MODE_BUFFER;

    /**
     * The input mode where the client renders the images to an input Surface
     * created by the writer.
     *
     * The input surface operates in single buffer mode. As a result, for use case
     * where camera directly outputs to the input surface, this mode will not work
     * because camera framework requires multiple buffers to operate in a pipeline
     * fashion.
     *
     * @see #getInputSurface()
     */
    public static final int INPUT_MODE_SURFACE = WriterBase.INPUT_MODE_SURFACE;

    /**
     * The input mode where the client adds bitmaps.
     *
     * @see #addBitmap(Bitmap)
     */
    public static final int INPUT_MODE_BITMAP = WriterBase.INPUT_MODE_BITMAP;

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @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 in number of pixels.
         * @param height Height of the image in number of pixels.
         * @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,
            @IntRange(from = 1) int width,
            @IntRange(from = 1) 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 in number of pixels.
         * @param height Height of the image in number of pixels.
         * @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,
            @IntRange(from = 1) int width,
            @IntRange(from = 1) int height,
            @InputMode int inputMode) {
            this(null, fd, width, height, inputMode);
        }

        private Builder(String path, FileDescriptor fd,
            @IntRange(from = 1) int width,
            @IntRange(from = 1) int height,
            @InputMode int inputMode) {
            mPath = path;
            mFd = fd;
            mWidth = width;
            mHeight = height;
            mInputMode = inputMode;
        }

        /**
         * Set the image rotation in degrees.
         *
         * @param rotation Rotation angle in degrees (clockwise) of the image, must be 0, 90,
         *                 180 or 270. Default is 0.
         * @return this Builder object.
         */
        public @NonNull Builder setRotation(@IntRange(from = 0)  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 @NonNull 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 @NonNull Builder setQuality(@IntRange(from = 0, to = 100) 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 @NonNull Builder setMaxImages(@IntRange(from = 1) 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 @NonNull Builder setPrimaryIndex(@IntRange(from = 0) int 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 @NonNull 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 @NonNull HeifWriter build() throws IOException {
            return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
                mMaxImages, mPrimaryIndex, mInputMode, mHandler);
        }
    }

    @SuppressLint("WrongConstant")
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    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 {
        super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
            handler, /* highBitDepthEnabled */ false);

        if (DEBUG) {
            Log.d(TAG, "width: " + width
                + ", height: " + height
                + ", rotation: " + rotation
                + ", gridEnabled: " + gridEnabled
                + ", quality: " + quality
                + ", maxImages: " + maxImages
                + ", primaryIndex: " + primaryIndex
                + ", inputMode: " + inputMode);
        }

        // 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());
    }
}