AvifEncoder.java

/*
 * Copyright 2022 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 android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Range;

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

import java.io.IOException;

/**
 * This class encodes images into HEIF-compatible samples using AV1 encoder.
 *
 * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
 * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
 *
 * The output format and samples are sent back in {@link
 * Callback#onOutputFormatChanged(HeifEncoder, MediaFormat)} and {@link
 * Callback#onDrainOutputBuffer(HeifEncoder, ByteBuffer)}. If the client
 * requests to use grid, each tile will be sent back individually.
 *
 * HeifEncoder is made a separate class from {@link HeifWriter}, as some more
 * advanced use cases might want to build solutions on top of the HeifEncoder directly.
 * (eg. mux still images and video tracks into a single container).
 *
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class AvifEncoder extends EncoderBase {
    private static final String TAG = "AvifEncoder";
    private static final boolean DEBUG = false;

    protected static final int GRID_WIDTH = 512;
    protected static final int GRID_HEIGHT = 512;
    // Block size for AV1 encoder
    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.
     *
     * @param width Width of the image.
     * @param height Height of the image.
     * @param useGrid Whether to encode image into tiles. If enabled, 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 inputMode The input type of this encoding session.
     * @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 us.
     * @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,
            @Nullable Handler handler, @NonNull Callback cb,
            boolean useBitDepth10) throws IOException {
        super("AVIF", width, height, useGrid, quality, inputMode, 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.
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    protected class Av1EncoderCallback extends EncoderCallback {
        @Override
        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
            if (codec != mEncoder) return;

            if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);

            // TODO(b/252835975) replace "image/avif" with  MIMETYPE_IMAGE_AVIF.
            if (!format.getString(MediaFormat.KEY_MIME).equals("image/avif")) {
                format.setString(MediaFormat.KEY_MIME, "image/avif");
                format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
                format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);

                if (mUseGrid) {
                    format.setInteger(MediaFormat.KEY_TILE_WIDTH, mGridWidth);
                    format.setInteger(MediaFormat.KEY_TILE_HEIGHT, mGridHeight);
                    format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
                    format.setInteger(MediaFormat.KEY_GRID_COLUMNS, mGridCols);
                }
            }

            mCallback.onOutputFormatChanged(AvifEncoder.this, format);
        }
    }
}