[go: nahoru, domu]

Duplicate camera-core/VideoCapture to camera-video/VideoCaptureLeagcy

Copy camera-core/VideoCapture.java to camera-video/VideoCaptureLegacy.
And also made relative changes.
And Move camera-core/test/VideoCaptureTest.kt to
camera-video/test/VideoCaptureLegacyTest.kt.

Bug: b/172306036
Test: ./gradlew buildOnServer
Change-Id: I2314cfafce04d4bdc3136a6206d521365b709397
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index 7bc5694..fc65c8e 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -22,11 +22,27 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("kotlin-android")
 }
 
 dependencies {
     api(project(":camera:camera-core"))
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    testImplementation 'junit:junit:4.12'
+
     annotationProcessor(AUTO_VALUE)
+
+    testImplementation(KOTLIN_STDLIB)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(JUNIT)
+    testImplementation(TRUTH)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+    testImplementation project(":camera:camera-testing"), {
+        exclude group: "androidx.camera", module: "camera-core"
+    }
 }
 
 android {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
new file mode 100644
index 0000000..8e0613f
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -0,0 +1,1847 @@
+/*
+ * Copyright (C) 2019 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.camera.video;
+
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
+import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
+import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_BIT_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_CHANNEL_COUNT;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_MIN_BUFFER_SIZE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_RECORD_SOURCE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_SAMPLE_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_BIT_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_INTRA_FRAME_INTERVAL;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_VIDEO_FRAME_RATE;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.location.Location;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.CamcorderProfile;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.media.MediaRecorder.AudioSource;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraXThreads;
+import androidx.camera.core.Logger;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ConfigProvider;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.ImmediateSurface;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.core.internal.utils.VideoUtil;
+import androidx.camera.video.impl.VideoCaptureLegacyConfig;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+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.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A use case for taking a video.
+ *
+ * <p>This class is designed for simple video capturing. It gives basic configuration of the
+ * recorded video such as resolution and file format.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class VideoCaptureLegacy extends UseCase {
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
+    // they could be created in the constructor.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * An unknown error occurred.
+     *
+     * <p>See message parameter in onError callback or log for more details.
+     */
+    public static final int ERROR_UNKNOWN = 0;
+    /**
+     * An error occurred with encoder state, either when trying to change state or when an
+     * unexpected state change occurred.
+     */
+    public static final int ERROR_ENCODER = 1;
+    /** An error with muxer state such as during creation or when stopping. */
+    public static final int ERROR_MUXER = 2;
+    /**
+     * An error indicating start recording was called when video recording is still in progress.
+     */
+    public static final int ERROR_RECORDING_IN_PROGRESS = 3;
+    /**
+     * An error indicating the file saving operations.
+     */
+    public static final int ERROR_FILE_IO = 4;
+    /**
+     * An error indicating this VideoCaptureLegacy is not bound to a camera.
+     */
+    public static final int ERROR_INVALID_CAMERA = 5;
+
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final String TAG = "VideoCaptureLegacy";
+    /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
+    private static final int DEQUE_TIMEOUT_USEC = 10000;
+    /** Android preferred mime type for AVC video. */
+    private static final String VIDEO_MIME_TYPE = "video/avc";
+    private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
+    /** Camcorder profiles quality list */
+    private static final int[] CamcorderQuality = {
+            CamcorderProfile.QUALITY_2160P,
+            CamcorderProfile.QUALITY_1080P,
+            CamcorderProfile.QUALITY_720P,
+            CamcorderProfile.QUALITY_480P
+    };
+    /**
+     * Audio encoding
+     *
+     * <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
+     */
+    private static final short[] sAudioEncoding = {
+            AudioFormat.ENCODING_PCM_16BIT,
+            AudioFormat.ENCODING_PCM_8BIT,
+            AudioFormat.ENCODING_PCM_FLOAT
+    };
+
+    private final BufferInfo mVideoBufferInfo = new BufferInfo();
+    private final Object mMuxerLock = new Object();
+    private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
+    private final BufferInfo mAudioBufferInfo = new BufferInfo();
+    /** For record the first sample written time. */
+    private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
+    private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /** Thread on which all encoding occurs. */
+    private HandlerThread mVideoHandlerThread;
+    private Handler mVideoHandler;
+    /** Thread on which audio encoding occurs. */
+    private HandlerThread mAudioHandlerThread;
+    private Handler mAudioHandler;
+
+    @NonNull
+    MediaCodec mVideoEncoder;
+    @NonNull
+    private MediaCodec mAudioEncoder;
+    @Nullable
+    private ListenableFuture<Void> mRecordingFuture = null;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /** The muxer that writes the encoding data to file. */
+    @GuardedBy("mMuxerLock")
+    private MediaMuxer mMuxer;
+    private boolean mMuxerStarted = false;
+    /** The index of the video track used by the muxer. */
+    private int mVideoTrackIndex;
+    /** The index of the audio track used by the muxer. */
+    private int mAudioTrackIndex;
+    /** Surface the camera writes to, which the videoEncoder uses as input. */
+    Surface mCameraSurface;
+
+    /** audio raw data */
+    @NonNull
+    private AudioRecord mAudioRecorder;
+    private int mAudioBufferSize;
+    private boolean mIsRecording = false;
+    private int mAudioChannelCount;
+    private int mAudioSampleRate;
+    private int mAudioBitRate;
+    private DeferrableSurface mDeferrableSurface;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    Uri mSavedVideoUri;
+    private ParcelFileDescriptor mParcelFileDescriptor;
+
+    /**
+     * Creates a new video capture use case from the given configuration.
+     *
+     * @param config for this use case instance
+     */
+    VideoCaptureLegacy(@NonNull VideoCaptureLegacyConfig config) {
+        super(config);
+    }
+
+    /** Creates a {@link MediaFormat} using parameters from the configuration */
+    private static MediaFormat createMediaFormat(VideoCaptureLegacyConfig config, Size resolution) {
+        MediaFormat format =
+                MediaFormat.createVideoFormat(
+                        VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
+        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
+
+        return format;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
+            @NonNull UseCaseConfigFactory factory) {
+        Config captureConfig = factory.getConfig(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE);
+
+        if (applyDefaultConfig) {
+            captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
+        }
+
+        return captureConfig == null ? null :
+                getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @SuppressWarnings("WrongConstant")
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void onAttached() {
+        mVideoHandlerThread = new HandlerThread(CameraXThreads.TAG + "video encoding thread");
+        mAudioHandlerThread = new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
+
+        // video thread start
+        mVideoHandlerThread.start();
+        mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
+
+        // audio thread start
+        mAudioHandlerThread.start();
+        mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+        if (mCameraSurface != null) {
+            mVideoEncoder.stop();
+            mVideoEncoder.release();
+            mAudioEncoder.stop();
+            mAudioEncoder.release();
+            releaseCameraSurface(false);
+        }
+
+        try {
+            mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
+            mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
+        }
+
+        setupEncoder(getCameraId(), suggestedResolution);
+        return suggestedResolution;
+    }
+
+    /**
+     * Starts recording video, which continues until {@link VideoCaptureLegacy#stopRecording()} is
+     * called.
+     *
+     * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+     * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
+     *
+     * @param outputFileOptions Location to save the video capture
+     * @param executor          The executor in which the callback methods will be run.
+     * @param callback          Callback for when the recorded video saving completion or failure.
+     */
+    public void startRecording(
+            @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
+            @NonNull OnVideoSavedCallback callback) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            CameraXExecutors.mainThreadExecutor().execute(() -> startRecording(outputFileOptions,
+                    executor, callback));
+            return;
+        }
+        Logger.i(TAG, "startRecording");
+        mIsFirstVideoSampleWrite.set(false);
+        mIsFirstAudioSampleWrite.set(false);
+
+        OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
+
+        CameraInternal attachedCamera = getCamera();
+        if (attachedCamera == null) {
+            // Not bound. Notify callback.
+            postListener.onError(ERROR_INVALID_CAMERA,
+                    "Not bound to a Camera [" + VideoCaptureLegacy.this + "]", null);
+            return;
+        }
+
+        if (!mEndOfAudioVideoSignal.get()) {
+            postListener.onError(
+                    ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
+                    null);
+            return;
+        }
+
+        try {
+            // audioRecord start
+            mAudioRecorder.startRecording();
+        } catch (IllegalStateException e) {
+            postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
+            return;
+        }
+
+        AtomicReference<Completer<Void>> recordingCompleterRef = new AtomicReference<>();
+        mRecordingFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    recordingCompleterRef.set(completer);
+                    return "startRecording";
+                });
+        Completer<Void> recordingCompleter =
+                Preconditions.checkNotNull(recordingCompleterRef.get());
+
+        mRecordingFuture.addListener(() -> {
+            mRecordingFuture = null;
+            // Do the setup of the videoEncoder at the end of video recording instead of at the
+            // start of recording because it requires attaching a new Surface. This causes a
+            // glitch so we don't want that to incur latency at the start of capture.
+            if (getCamera() != null) {
+                // Ensure the use case is bound. Asynchronous stopping procedure may occur after
+                // the use case is unbound, i.e. after onDetached().
+                setupEncoder(getCameraId(), getAttachedSurfaceResolution());
+                notifyReset();
+            }
+        }, CameraXExecutors.mainThreadExecutor());
+
+        try {
+            // video encoder start
+            Logger.i(TAG, "videoEncoder start");
+            mVideoEncoder.start();
+            // audio encoder start
+            Logger.i(TAG, "audioEncoder start");
+            mAudioEncoder.start();
+
+        } catch (IllegalStateException e) {
+            recordingCompleter.set(null);
+            postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail", e);
+            return;
+        }
+
+        try {
+            synchronized (mMuxerLock) {
+                mMuxer = initMediaMuxer(outputFileOptions);
+                Preconditions.checkNotNull(mMuxer);
+                mMuxer.setOrientationHint(getRelativeRotation(attachedCamera));
+
+                Metadata metadata = outputFileOptions.getMetadata();
+                if (metadata != null && metadata.location != null) {
+                    mMuxer.setLocation(
+                            (float) metadata.location.getLatitude(),
+                            (float) metadata.location.getLongitude());
+                }
+            }
+        } catch (IOException e) {
+            recordingCompleter.set(null);
+            postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
+            return;
+        }
+
+        mEndOfVideoStreamSignal.set(false);
+        mEndOfAudioStreamSignal.set(false);
+        mEndOfAudioVideoSignal.set(false);
+        mIsRecording = true;
+
+        notifyActive();
+        mAudioHandler.post(() -> audioEncode(postListener));
+
+        String cameraId = getCameraId();
+        Size resolution = getAttachedSurfaceResolution();
+        mVideoHandler.post(
+                () -> {
+                    boolean errorOccurred = videoEncode(postListener, cameraId, resolution);
+                    if (!errorOccurred) {
+                        postListener.onVideoSaved(new OutputFileResults(mSavedVideoUri));
+                        mSavedVideoUri = null;
+                    }
+                    recordingCompleter.set(null);
+                });
+    }
+
+    /**
+     * Stops recording video, this must be called after {@link
+     * VideoCaptureLegacy#startRecording(OutputFileOptions, Executor, OnVideoSavedCallback)} is
+     * called.
+     *
+     * <p>stopRecording() is asynchronous API. User need to check if {@link
+     * OnVideoSavedCallback#onVideoSaved(OutputFileResults)} or
+     * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
+     * before startRecording.
+     */
+    public void stopRecording() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            CameraXExecutors.mainThreadExecutor().execute(() -> stopRecording());
+            return;
+        }
+        Logger.i(TAG, "stopRecording");
+        notifyInactive();
+        if (!mEndOfAudioVideoSignal.get() && mIsRecording) {
+            // stop audio encoder thread, and wait video encoder and muxer stop.
+            mEndOfAudioStreamSignal.set(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void onDetached() {
+        stopRecording();
+
+        if (mRecordingFuture != null) {
+            mRecordingFuture.addListener(() -> releaseResources(),
+                    CameraXExecutors.mainThreadExecutor());
+        } else {
+            releaseResources();
+        }
+    }
+
+    private void releaseResources() {
+        mVideoHandlerThread.quitSafely();
+
+        // audio encoder release
+        mAudioHandlerThread.quitSafely();
+        if (mAudioEncoder != null) {
+            mAudioEncoder.release();
+            mAudioEncoder = null;
+        }
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+            mAudioRecorder = null;
+        }
+
+        if (mCameraSurface != null) {
+            releaseCameraSurface(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
+        return Builder.fromConfig(config);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @UiThread
+    @Override
+    public void onStateDetached() {
+        stopRecording();
+    }
+
+    @UiThread
+    private void releaseCameraSurface(final boolean releaseVideoEncoder) {
+        if (mDeferrableSurface == null) {
+            return;
+        }
+
+        final MediaCodec videoEncoder = mVideoEncoder;
+
+        // Calling close should allow termination future to complete and close the surface with
+        // the listener that was added after constructing the DeferrableSurface.
+        mDeferrableSurface.close();
+        mDeferrableSurface.getTerminationFuture().addListener(
+                () -> {
+                    if (releaseVideoEncoder && videoEncoder != null) {
+                        videoEncoder.release();
+                    }
+                }, CameraXExecutors.mainThreadExecutor());
+
+        if (releaseVideoEncoder) {
+            mVideoEncoder = null;
+        }
+        mCameraSurface = null;
+        mDeferrableSurface = null;
+    }
+
+
+    /**
+     * Sets the desired rotation of the output video.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output video.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        setTargetRotationInternal(rotation);
+    }
+
+    /**
+     * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
+     * audio from selected audio source.
+     */
+    @UiThread
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
+        VideoCaptureLegacyConfig config = (VideoCaptureLegacyConfig) getCurrentConfig();
+
+        // video encoder setup
+        mVideoEncoder.reset();
+        mVideoEncoder.configure(
+                createMediaFormat(config, resolution), /*surface*/
+                null, /*crypto*/
+                null,
+                MediaCodec.CONFIGURE_FLAG_ENCODE);
+        if (mCameraSurface != null) {
+            releaseCameraSurface(false);
+        }
+        Surface cameraSurface = mVideoEncoder.createInputSurface();
+        mCameraSurface = cameraSurface;
+
+        SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+
+        if (mDeferrableSurface != null) {
+            mDeferrableSurface.close();
+        }
+        mDeferrableSurface = new ImmediateSurface(mCameraSurface);
+        mDeferrableSurface.getTerminationFuture().addListener(
+                cameraSurface::release, CameraXExecutors.mainThreadExecutor()
+        );
+
+        sessionConfigBuilder.addSurface(mDeferrableSurface);
+
+        sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
+            @Override
+            public void onError(@NonNull SessionConfig sessionConfig,
+                    @NonNull SessionConfig.SessionError error) {
+                // Ensure the attached camera has not changed before calling setupEncoder.
+                // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
+                //  to this use case so we don't need to do this check.
+                if (isCurrentCamera(cameraId)) {
+                    // Only reset the pipeline when the bound camera is the same.
+                    setupEncoder(cameraId, resolution);
+                }
+            }
+        });
+
+        updateSessionConfig(sessionConfigBuilder.build());
+
+        // audio encoder setup
+        setAudioParametersByCamcorderProfile(resolution, cameraId);
+        mAudioEncoder.reset();
+        mAudioEncoder.configure(
+                createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+        }
+        mAudioRecorder = autoConfigAudioRecordSource(config);
+        // check mAudioRecorder
+        if (mAudioRecorder == null) {
+            Logger.e(TAG, "AudioRecord object cannot initialized correctly!");
+        }
+
+        mVideoTrackIndex = -1;
+        mAudioTrackIndex = -1;
+        mIsRecording = false;
+    }
+
+    /**
+     * Write a buffer that has been encoded to file.
+     *
+     * @param bufferIndex the index of the buffer in the videoEncoder that has available data
+     * @return returns true if this buffer is the end of the stream
+     */
+    private boolean writeVideoEncodedBuffer(int bufferIndex) {
+        if (bufferIndex < 0) {
+            Logger.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
+            return false;
+        }
+        // Get data from buffer
+        ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
+
+        // Check if buffer is valid, if not then return
+        if (outputBuffer == null) {
+            Logger.d(TAG, "OutputBuffer was null.");
+            return false;
+        }
+
+        // Write data to mMuxer if available
+        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0 && mVideoBufferInfo.size > 0) {
+            outputBuffer.position(mVideoBufferInfo.offset);
+            outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
+            mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
+
+            synchronized (mMuxerLock) {
+                if (!mIsFirstVideoSampleWrite.get()) {
+                    Logger.i(TAG, "First video sample written.");
+                    mIsFirstVideoSampleWrite.set(true);
+                }
+                mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
+            }
+        }
+
+        // Release data
+        mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
+
+        // Return true if EOS is set
+        return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    private boolean writeAudioEncodedBuffer(int bufferIndex) {
+        ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
+        buffer.position(mAudioBufferInfo.offset);
+        if (mAudioTrackIndex >= 0
+                && mVideoTrackIndex >= 0
+                && mAudioBufferInfo.size > 0
+                && mAudioBufferInfo.presentationTimeUs > 0) {
+            try {
+                synchronized (mMuxerLock) {
+                    if (!mIsFirstAudioSampleWrite.get()) {
+                        Logger.i(TAG, "First audio sample written.");
+                        mIsFirstAudioSampleWrite.set(true);
+                    }
+                    mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
+                }
+            } catch (Exception e) {
+                Logger.e(
+                        TAG,
+                        "audio error:size="
+                                + mAudioBufferInfo.size
+                                + "/offset="
+                                + mAudioBufferInfo.offset
+                                + "/timeUs="
+                                + mAudioBufferInfo.presentationTimeUs);
+                e.printStackTrace();
+            }
+        }
+        mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
+        return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    /**
+     * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
+     * main thread otherwise it will cause the application to block.
+     *
+     * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
+     */
+    boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
+            @NonNull Size resolution) {
+        // Main encoding loop. Exits on end of stream.
+        boolean errorOccurred = false;
+        boolean videoEos = false;
+        while (!videoEos && !errorOccurred) {
+            // Check for end of stream from main thread
+            if (mEndOfVideoStreamSignal.get()) {
+                mVideoEncoder.signalEndOfInputStream();
+                mEndOfVideoStreamSignal.set(false);
+            }
+
+            // Deque buffer to check for processing step
+            int outputBufferId =
+                    mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
+            switch (outputBufferId) {
+                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                    if (mMuxerStarted) {
+                        videoSavedCallback.onError(
+                                ERROR_ENCODER,
+                                "Unexpected change in video encoding format.",
+                                null);
+                        errorOccurred = true;
+                    }
+
+                    synchronized (mMuxerLock) {
+                        mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
+                        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                            mMuxerStarted = true;
+                            Logger.i(TAG, "media mMuxer start");
+                            mMuxer.start();
+                        }
+                    }
+                    break;
+                case MediaCodec.INFO_TRY_AGAIN_LATER:
+                    // Timed out. Just wait until next attempt to deque.
+                    break;
+                default:
+                    videoEos = writeVideoEncodedBuffer(outputBufferId);
+            }
+        }
+
+        try {
+            Logger.i(TAG, "videoEncoder stop");
+            mVideoEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_ENCODER,
+                    "Video encoder stop failed!", e);
+            errorOccurred = true;
+        }
+
+        try {
+            // new MediaMuxer instance required for each new file written, and release current one.
+            synchronized (mMuxerLock) {
+                if (mMuxer != null) {
+                    if (mMuxerStarted) {
+                        mMuxer.stop();
+                    }
+                    mMuxer.release();
+                    mMuxer = null;
+                }
+            }
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
+            errorOccurred = true;
+        }
+
+        if (mParcelFileDescriptor != null) {
+            try {
+                mParcelFileDescriptor.close();
+                mParcelFileDescriptor = null;
+            } catch (IOException e) {
+                videoSavedCallback.onError(ERROR_MUXER, "File descriptor close failed!", e);
+                errorOccurred = true;
+            }
+        }
+
+        mMuxerStarted = false;
+
+        // notify the UI thread that the video recording has finished
+        mEndOfAudioVideoSignal.set(true);
+
+        Logger.i(TAG, "Video encode thread end.");
+        return errorOccurred;
+    }
+
+    boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
+        // Audio encoding loop. Exits on end of stream.
+        boolean audioEos = false;
+        int outIndex;
+        while (!audioEos && mIsRecording) {
+            // Check for end of stream from main thread
+            if (mEndOfAudioStreamSignal.get()) {
+                mEndOfAudioStreamSignal.set(false);
+                mIsRecording = false;
+            }
+
+            // get audio deque input buffer
+            if (mAudioEncoder != null && mAudioRecorder != null) {
+                int index = mAudioEncoder.dequeueInputBuffer(-1);
+                if (index >= 0) {
+                    final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
+                    buffer.clear();
+                    int length = mAudioRecorder.read(buffer, mAudioBufferSize);
+                    if (length > 0) {
+                        mAudioEncoder.queueInputBuffer(
+                                index,
+                                0,
+                                length,
+                                (System.nanoTime() / 1000),
+                                mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                    }
+                }
+
+                // start to dequeue audio output buffer
+                do {
+                    outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
+                    switch (outIndex) {
+                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                            synchronized (mMuxerLock) {
+                                mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
+                                if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                                    mMuxerStarted = true;
+                                    mMuxer.start();
+                                }
+                            }
+                            break;
+                        case MediaCodec.INFO_TRY_AGAIN_LATER:
+                            break;
+                        default:
+                            audioEos = writeAudioEncodedBuffer(outIndex);
+                    }
+                } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
+            }
+        } // end of while loop
+
+        // Audio Stop
+        try {
+            Logger.i(TAG, "audioRecorder stop");
+            mAudioRecorder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(
+                    ERROR_ENCODER, "Audio recorder stop failed!", e);
+        }
+
+        try {
+            mAudioEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_ENCODER,
+                    "Audio encoder stop failed!", e);
+        }
+
+        Logger.i(TAG, "Audio encode thread end");
+        // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
+        // safe
+        mEndOfVideoStreamSignal.set(true);
+
+        return false;
+    }
+
+    private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
+        return codec.getInputBuffer(index);
+    }
+
+    private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
+        return codec.getOutputBuffer(index);
+    }
+
+    /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
+    private MediaFormat createAudioMediaFormat() {
+        MediaFormat format =
+                MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
+                        mAudioChannelCount);
+        format.setInteger(
+                MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
+
+        return format;
+    }
+
+    /** Create a AudioRecord object to get raw data */
+    private AudioRecord autoConfigAudioRecordSource(VideoCaptureLegacyConfig config) {
+        for (short audioFormat : sAudioEncoding) {
+
+            // Use channel count to determine stereo vs mono
+            int channelConfig =
+                    mAudioChannelCount == 1
+                            ? AudioFormat.CHANNEL_IN_MONO
+                            : AudioFormat.CHANNEL_IN_STEREO;
+            int source = config.getAudioRecordSource();
+
+            try {
+                int bufferSize =
+                        AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
+
+                if (bufferSize <= 0) {
+                    bufferSize = config.getAudioMinBufferSize();
+                }
+
+                AudioRecord recorder =
+                        new AudioRecord(
+                                source,
+                                mAudioSampleRate,
+                                channelConfig,
+                                audioFormat,
+                                bufferSize * 2);
+
+                if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
+                    mAudioBufferSize = bufferSize;
+                    Logger.i(
+                            TAG,
+                            "source: "
+                                    + source
+                                    + " audioSampleRate: "
+                                    + mAudioSampleRate
+                                    + " channelConfig: "
+                                    + channelConfig
+                                    + " audioFormat: "
+                                    + audioFormat
+                                    + " bufferSize: "
+                                    + bufferSize);
+                    return recorder;
+                }
+            } catch (Exception e) {
+                Logger.e(TAG, "Exception, keep trying.", e);
+            }
+        }
+
+        return null;
+    }
+
+    /** Set audio record parameters by CamcorderProfile */
+    private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
+        CamcorderProfile profile;
+        boolean isCamcorderProfileFound = false;
+
+        try {
+            for (int quality : CamcorderQuality) {
+                if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
+                    profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
+                    if (currentResolution.getWidth() == profile.videoFrameWidth
+                            && currentResolution.getHeight() == profile.videoFrameHeight) {
+                        mAudioChannelCount = profile.audioChannels;
+                        mAudioSampleRate = profile.audioSampleRate;
+                        mAudioBitRate = profile.audioBitRate;
+                        isCamcorderProfileFound = true;
+                        break;
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            Logger.i(TAG, "The camera Id is not an integer because the camera may be a removable "
+                    + "device. Use the default values for the audio related settings.");
+        }
+
+        // In case no corresponding camcorder profile can be founded, * get default value from
+        // VideoCaptureConfig.
+        if (!isCamcorderProfileFound) {
+            VideoCaptureLegacyConfig config = (VideoCaptureLegacyConfig) getCurrentConfig();
+            mAudioChannelCount = config.getAudioChannelCount();
+            mAudioSampleRate = config.getAudioSampleRate();
+            mAudioBitRate = config.getAudioBitRate();
+        }
+    }
+
+    @SuppressLint("UnsafeNewApiCall")
+    @NonNull
+    private MediaMuxer initMediaMuxer(@NonNull OutputFileOptions outputFileOptions)
+            throws IOException {
+        MediaMuxer mediaMuxer;
+
+        if (outputFileOptions.isSavingToFile()) {
+            File savedVideoFile = outputFileOptions.getFile();
+            mSavedVideoUri = Uri.fromFile(outputFileOptions.getFile());
+
+            mediaMuxer = new MediaMuxer(savedVideoFile.getAbsolutePath(),
+                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+        } else if (outputFileOptions.isSavingToFileDescriptor()) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                throw new IllegalArgumentException("Using a FileDescriptor to record a video is "
+                        + "only supported for Android 8.0 or above.");
+            }
+
+            mediaMuxer = new MediaMuxer(outputFileOptions.getFileDescriptor(),
+                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+        } else if (outputFileOptions.isSavingToMediaStore()) {
+            ContentValues values = outputFileOptions.getContentValues() != null
+                    ? new ContentValues(outputFileOptions.getContentValues())
+                    : new ContentValues();
+
+            mSavedVideoUri = outputFileOptions.getContentResolver().insert(
+                    outputFileOptions.getSaveCollection(), values);
+
+            if (mSavedVideoUri == null) {
+                throw new IOException("Invalid Uri!");
+            }
+
+            // Sine API 26, media muxer could be initiated by a FileDescriptor.
+            try {
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                    String savedLocationPath = VideoUtil.getAbsolutePathFromUri(
+                            outputFileOptions.getContentResolver(), mSavedVideoUri);
+
+                    Logger.i(TAG, "Saved Location Path: " + savedLocationPath);
+                    mediaMuxer = new MediaMuxer(savedLocationPath,
+                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+                } else {
+                    mParcelFileDescriptor =
+                            outputFileOptions.getContentResolver().openFileDescriptor(
+                                    mSavedVideoUri, "rw");
+                    mediaMuxer = new MediaMuxer(mParcelFileDescriptor.getFileDescriptor(),
+                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+                }
+            } catch (IOException e) {
+                mSavedVideoUri = null;
+                throw e;
+            }
+        } else {
+            throw new IllegalArgumentException(
+                    "The OutputFileOptions should assign before recording");
+        }
+
+        return mediaMuxer;
+    }
+
+    /**
+     * Describes the error that occurred during video capture operations.
+     *
+     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+     * OnVideoSavedCallback#onError(int, String, Throwable)}.
+     *
+     * <p>See message parameter in onError callback or log for more details.
+     *
+     * @hide
+     */
+    @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
+            ERROR_FILE_IO, ERROR_INVALID_CAMERA})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public @interface VideoCaptureError {
+    }
+
+    /** Listener containing callbacks for video file I/O events. */
+    public interface OnVideoSavedCallback {
+        /** Called when the video has been successfully saved. */
+        void onVideoSaved(@NonNull OutputFileResults outputFileResults);
+
+        /** Called when an error occurs while attempting to save the video. */
+        void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
+                @Nullable Throwable cause);
+    }
+
+    /**
+     * Provides a base static default configuration for the VideoCapture
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigProvider<VideoCaptureLegacyConfig> {
+        private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+        /** 8Mb/s the recommend rate for 30fps 1080p */
+        private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
+        /** Seconds between each key frame */
+        private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
+        /** audio bit rate */
+        private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
+        /** audio sample rate */
+        private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
+        /** audio channel count */
+        private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
+        /** audio record source */
+        private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
+        /** audio default minimum buffer size */
+        private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
+        /** Current max resolution of VideoCaptureLegacy is set as FHD */
+        private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+        /** Surface occupancy prioirty to this use case */
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+        private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9;
+
+        private static final VideoCaptureLegacyConfig DEFAULT_CONFIG;
+
+        static {
+            Builder builder = new Builder()
+                    .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
+                    .setBitRate(DEFAULT_BIT_RATE)
+                    .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
+                    .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
+                    .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
+                    .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
+                    .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
+                    .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
+                    .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+
+            DEFAULT_CONFIG = builder.getUseCaseConfig();
+        }
+
+        @NonNull
+        @Override
+        public VideoCaptureLegacyConfig getConfig() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    /** Holder class for metadata that should be saved alongside captured video. */
+    public static final class Metadata {
+        /** Data representing a geographic location. */
+        @Nullable
+        public Location location;
+    }
+
+    private static final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
+
+        @NonNull
+        Executor mExecutor;
+        @NonNull
+        OnVideoSavedCallback mOnVideoSavedCallback;
+
+        VideoSavedListenerWrapper(@NonNull Executor executor,
+                @NonNull OnVideoSavedCallback onVideoSavedCallback) {
+            mExecutor = executor;
+            mOnVideoSavedCallback = onVideoSavedCallback;
+        }
+
+        @Override
+        public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
+            try {
+                mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(outputFileResults));
+            } catch (RejectedExecutionException e) {
+                Logger.e(TAG, "Unable to post to the supplied executor.");
+            }
+        }
+
+        @Override
+        public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
+                @Nullable Throwable cause) {
+            try {
+                mExecutor.execute(
+                        () -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
+            } catch (RejectedExecutionException e) {
+                Logger.e(TAG, "Unable to post to the supplied executor.");
+            }
+        }
+
+    }
+
+    /** Builder for a {@link VideoCaptureLegacy}. */
+    public static final class Builder
+            implements
+            UseCaseConfig.Builder<VideoCaptureLegacy, VideoCaptureLegacyConfig, Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder> {
+
+        private final MutableOptionsBundle mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(@NonNull MutableOptionsBundle mutableConfig) {
+            mMutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(VideoCaptureLegacy.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(VideoCaptureLegacy.class);
+        }
+
+        /**
+         * Generates a Builder from another Config object.
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        static Builder fromConfig(@NonNull Config configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+
+        /**
+         * Generates a Builder from another Config object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        @NonNull
+        public static Builder fromConfig(@NonNull VideoCaptureLegacyConfig configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public MutableConfig getMutableConfig() {
+            return mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public VideoCaptureLegacyConfig getUseCaseConfig() {
+            return new VideoCaptureLegacyConfig(OptionsBundle.from(mMutableConfig));
+        }
+
+        /**
+         * Builds an immutable {@link VideoCaptureLegacyConfig} from the current state.
+         *
+         * @return A {@link VideoCaptureLegacyConfig} populated with the current state.
+         */
+        @Override
+        @NonNull
+        public VideoCaptureLegacy build() {
+            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
+            // the same config.
+            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
+                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
+                throw new IllegalArgumentException(
+                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+                                + "config.");
+            }
+            return new VideoCaptureLegacy(getUseCaseConfig());
+        }
+
+        /**
+         * Sets the recording frames per second.
+         *
+         * @param videoFrameRate The requested interval in seconds.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setVideoFrameRate(int videoFrameRate) {
+            getMutableConfig().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
+            return this;
+        }
+
+        /**
+         * Sets the encoding bit rate.
+         *
+         * @param bitRate The requested bit rate in bits per second.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setBitRate(int bitRate) {
+            getMutableConfig().insertOption(OPTION_BIT_RATE, bitRate);
+            return this;
+        }
+
+        /**
+         * Sets number of seconds between each key frame in seconds.
+         *
+         * @param interval The requested interval in seconds.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setIFrameInterval(int interval) {
+            getMutableConfig().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
+            return this;
+        }
+
+        /**
+         * Sets the bit rate of the audio stream.
+         *
+         * @param bitRate The requested bit rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioBitRate(int bitRate) {
+            getMutableConfig().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
+            return this;
+        }
+
+        /**
+         * Sets the sample rate of the audio stream.
+         *
+         * @param sampleRate The requested sample rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioSampleRate(int sampleRate) {
+            getMutableConfig().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
+            return this;
+        }
+
+        /**
+         * Sets the number of audio channels.
+         *
+         * @param channelCount The requested number of audio channels.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioChannelCount(int channelCount) {
+            getMutableConfig().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
+            return this;
+        }
+
+        /**
+         * Sets the audio source.
+         *
+         * @param source The audio source. Currently only AudioSource.MIC is supported.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioRecordSource(int source) {
+            getMutableConfig().insertOption(OPTION_AUDIO_RECORD_SOURCE, source);
+            return this;
+        }
+
+        /**
+         * Sets the audio min buffer size.
+         *
+         * @param minBufferSize The requested audio minimum buffer size, in bytes.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioMinBufferSize(int minBufferSize) {
+            getMutableConfig().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
+            return this;
+        }
+
+        // Implementations of TargetConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setTargetClass(@NonNull Class<VideoCaptureLegacy> targetClass) {
+            getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+            // If no name is set yet, then generate a unique name
+            if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
+                String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+                setTargetName(targetName);
+            }
+
+            return this;
+        }
+
+        /**
+         * Sets the name of the target object being configured, used only for debug logging.
+         *
+         * <p>The name should be a value that can uniquely identify an instance of the object being
+         * configured.
+         *
+         * <p>If not set, the target name will default to an unique name automatically generated
+         * with the class canonical name and random UUID.
+         *
+         * @param targetName A unique string identifier for the instance of the class being
+         *                   configured.
+         * @return the current Builder.
+         */
+        @Override
+        @NonNull
+        public Builder setTargetName(@NonNull String targetName) {
+            getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
+            return this;
+        }
+
+        // Implementations of ImageOutputConfig.Builder default methods
+
+        /**
+         * Sets the aspect ratio of the intended target for images from this configuration.
+         *
+         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+         * use case.
+         *
+         * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
+         * ratio which may differ from the request, possibly due to device constraints.
+         * Application code should check the resulting output's resolution.
+         *
+         * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
+         * priority.
+         *
+         * @param aspectRatio A {@link AspectRatio} representing the ratio of the
+         *                    target's width and height.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
+            getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return this;
+        }
+
+        /**
+         * Sets the rotation of the intended target for images from this configuration.
+         *
+         * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+         * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+         * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+         *
+         * <p>If not set, the target rotation will default to the value of
+         * {@link Display#getRotation()} of the default display at the time the use case is
+         * created. The use case is fully created once it has been attached to a camera.
+         *
+         * @param rotation The rotation of the intended target.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return this;
+        }
+
+        /**
+         * Sets the resolution of the intended target from this configuration.
+         *
+         * <p>The target resolution attempts to establish a minimum bound for the image resolution.
+         * The actual image resolution will be the closest available resolution in size that is not
+         * smaller than the target resolution, as determined by the Camera implementation. However,
+         * if no resolution exists that is equal to or larger than the target resolution, the
+         * nearest available resolution smaller than the target resolution will be chosen.
+         *
+         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+         * use case.
+         *
+         * <p>The target aspect ratio will also be set the same as the aspect ratio of the provided
+         * {@link Size}. Make sure to set the target resolution with the correct orientation.
+         *
+         * @param resolution The target resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+            return this;
+        }
+
+        /**
+         * Sets the default resolution of the intended target from this configuration.
+         *
+         * @param resolution The default resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setDefaultResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setMaxResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
+            getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
+            return this;
+        }
+
+        // Implementations of ThreadConfig.Builder default methods
+
+        /**
+         * Sets the default executor that will be used for background tasks.
+         *
+         * <p>If not set, the background executor will default to an automatically generated
+         * {@link Executor}.
+         *
+         * @param executor The executor which will be used for background tasks.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setBackgroundExecutor(@NonNull Executor executor) {
+            getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
+            return this;
+        }
+
+        // Implementations of UseCaseConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSessionOptionUnpacker(
+                @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setCaptureOptionUnpacker(
+                @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY)
+        @Override
+        @NonNull
+        public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
+            getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setUseCaseEventCallback(
+                @NonNull EventCallback useCaseEventCallback) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
+            return this;
+        }
+    }
+
+    /**
+     * Info about the saved video file.
+     */
+    public static class OutputFileResults {
+        @Nullable
+        private Uri mSavedUri;
+
+        OutputFileResults(@Nullable Uri savedUri) {
+            mSavedUri = savedUri;
+        }
+
+        /**
+         * Returns the {@link Uri} of the saved video file.
+         *
+         * <p> This field is only returned if the {@link OutputFileOptions} is
+         * backed by {@link MediaStore} constructed with
+         * {@link OutputFileOptions}.
+         */
+        @Nullable
+        public Uri getSavedUri() {
+            return mSavedUri;
+        }
+    }
+
+    /**
+     * Options for saving newly captured video.
+     *
+     * <p> this class is used to configure save location and metadata. Save location can be
+     * either a {@link File}, {@link MediaStore}. The metadata will be
+     * stored with the saved video.
+     */
+    public static final class OutputFileOptions {
+
+        // Empty metadata object used as a placeholder for no user-supplied metadata.
+        // Should be initialized to all default values.
+        private static final Metadata EMPTY_METADATA = new Metadata();
+
+        @Nullable
+        private final File mFile;
+        @Nullable
+        private final FileDescriptor mFileDescriptor;
+        @Nullable
+        private final ContentResolver mContentResolver;
+        @Nullable
+        private final Uri mSaveCollection;
+        @Nullable
+        private final ContentValues mContentValues;
+        @Nullable
+        private final Metadata mMetadata;
+
+        OutputFileOptions(@Nullable File file,
+                @Nullable FileDescriptor fileDescriptor,
+                @Nullable ContentResolver contentResolver,
+                @Nullable Uri saveCollection,
+                @Nullable ContentValues contentValues,
+                @Nullable Metadata metadata) {
+            mFile = file;
+            mFileDescriptor = fileDescriptor;
+            mContentResolver = contentResolver;
+            mSaveCollection = saveCollection;
+            mContentValues = contentValues;
+            mMetadata = metadata == null ? EMPTY_METADATA : metadata;
+        }
+
+        /** Returns the File object which is set by the {@link Builder}. */
+        @Nullable
+        File getFile() {
+            return mFile;
+        }
+
+        /**
+         * Returns the FileDescriptor object which is set by the {@link Builder}.
+         */
+        @Nullable
+        FileDescriptor getFileDescriptor() {
+            return mFileDescriptor;
+        }
+
+        /** Returns the content resolver which is set by the {@link Builder}. */
+        @Nullable
+        ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+
+        /** Returns the URI which is set by the {@link Builder}. */
+        @Nullable
+        Uri getSaveCollection() {
+            return mSaveCollection;
+        }
+
+        /** Returns the content values which is set by the {@link Builder}. */
+        @Nullable
+        ContentValues getContentValues() {
+            return mContentValues;
+        }
+
+        /** Return the metadata which is set by the {@link Builder}.. */
+        @Nullable
+        Metadata getMetadata() {
+            return mMetadata;
+        }
+
+        /** Checking the caller wants to save video to MediaStore. */
+        boolean isSavingToMediaStore() {
+            return getSaveCollection() != null && getContentResolver() != null
+                    && getContentValues() != null;
+        }
+
+        /** Checking the caller wants to save video to a File. */
+        boolean isSavingToFile() {
+            return getFile() != null;
+        }
+
+        /** Checking the caller wants to save video to a FileDescriptor. */
+        boolean isSavingToFileDescriptor() {
+            return getFileDescriptor() != null;
+        }
+
+        /**
+         * Builder class for {@link OutputFileOptions}.
+         */
+        public static final class Builder {
+            @Nullable
+            private File mFile;
+            @Nullable
+            private FileDescriptor mFileDescriptor;
+            @Nullable
+            private ContentResolver mContentResolver;
+            @Nullable
+            private Uri mSaveCollection;
+            @Nullable
+            private ContentValues mContentValues;
+            @Nullable
+            private Metadata mMetadata;
+
+            /**
+             * Creates options to write captured video to a {@link File}.
+             *
+             * @param file save location of the video.
+             */
+            public Builder(@NonNull File file) {
+                mFile = file;
+            }
+
+            /**
+             * Creates options to write captured video to a {@link FileDescriptor}.
+             *
+             * <p>Using a FileDescriptor to record a video is only supported for Android 8.0 or
+             * above.
+             *
+             * @param fileDescriptor to save the video.
+             * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
+             */
+            public Builder(@NonNull FileDescriptor fileDescriptor) {
+                Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
+                        "Using a FileDescriptor to record a video is only supported for Android 8"
+                                + ".0 or above.");
+
+                mFileDescriptor = fileDescriptor;
+            }
+
+            /**
+             * Creates options to write captured video to {@link MediaStore}.
+             *
+             * Example:
+             *
+             * <pre>{@code
+             *
+             * ContentValues contentValues = new ContentValues();
+             * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
+             * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+             *
+             * OutputFileOptions options = new OutputFileOptions.Builder(
+             *         getContentResolver(),
+             *         MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+             *         contentValues).build();
+             *
+             * }</pre>
+             *
+             * @param contentResolver to access {@link MediaStore}
+             * @param saveCollection  The URL of the table to insert into.
+             * @param contentValues   to be included in the created video file.
+             */
+            public Builder(@NonNull ContentResolver contentResolver,
+                    @NonNull Uri saveCollection,
+                    @NonNull ContentValues contentValues) {
+                mContentResolver = contentResolver;
+                mSaveCollection = saveCollection;
+                mContentValues = contentValues;
+            }
+
+            /**
+             * Sets the metadata to be stored with the saved video.
+             *
+             * @param metadata Metadata to be stored with the saved video.
+             */
+            @NonNull
+            public Builder setMetadata(@NonNull Metadata metadata) {
+                mMetadata = metadata;
+                return this;
+            }
+
+            /**
+             * Builds {@link OutputFileOptions}.
+             */
+            @NonNull
+            public OutputFileOptions build() {
+                return new OutputFileOptions(mFile, mFileDescriptor, mContentResolver,
+                        mSaveCollection, mContentValues, mMetadata);
+            }
+        }
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java
new file mode 100644
index 0000000..9f67287
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2019 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.camera.video.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoCaptureLegacy;
+
+/**
+ * Config for a video capture use case.
+ *
+ * <p>In the earlier stage, the VideoCaptureLegacy is deprioritized.
+ */
+public final class VideoCaptureLegacyConfig
+        implements UseCaseConfig<VideoCaptureLegacy>,
+        ImageOutputConfig,
+        ThreadConfig {
+
+    // Option Declarations:
+    // *********************************************************************************************
+
+    public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.recordingFrameRate", int.class);
+    public static final Option<Integer> OPTION_BIT_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.bitRate", int.class);
+    public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
+            Option.create("camerax.video.videoCaptureLegacy.intraFrameInterval", int.class);
+    public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.audioBitRate", int.class);
+    public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.audioSampleRate", int.class);
+    public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
+            Option.create("camerax.video.videoCaptureLegacy.audioChannelCount", int.class);
+    public static final Option<Integer> OPTION_AUDIO_RECORD_SOURCE =
+            Option.create("camerax.video.videoCaptureLegacy.audioRecordSource", int.class);
+    public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
+            Option.create("camerax.video.videoCaptureLegacy.audioMinBufferSize", int.class);
+
+    // *********************************************************************************************
+
+    private final OptionsBundle mConfig;
+
+    public VideoCaptureLegacyConfig(@NonNull OptionsBundle config) {
+        mConfig = config;
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getVideoFrameRate(int valueIfMissing) {
+        return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getVideoFrameRate() {
+        return retrieveOption(OPTION_VIDEO_FRAME_RATE);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getBitRate(int valueIfMissing) {
+        return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getBitRate() {
+        return retrieveOption(OPTION_BIT_RATE);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getIFrameInterval(int valueIfMissing) {
+        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getIFrameInterval() {
+        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioBitRate(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioBitRate() {
+        return retrieveOption(OPTION_AUDIO_BIT_RATE);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioSampleRate(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioSampleRate() {
+        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioChannelCount(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioChannelCount() {
+        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioRecordSource(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_RECORD_SOURCE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioRecordSource() {
+        return retrieveOption(OPTION_AUDIO_RECORD_SOURCE);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioMinBufferSize(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioMinBufferSize() {
+        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
+    }
+
+    /**
+     * Retrieves the format of the image that is fed as input.
+     *
+     * <p>This should always be PRIVATE for VideoCaptureLegacy.
+     */
+    @Override
+    public int getInputFormat() {
+        return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+    }
+
+    @NonNull
+    @Override
+    public Config getConfig() {
+        return mConfig;
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java
new file mode 100644
index 0000000..1e6190c
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.video.impl;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
new file mode 100644
index 0000000..5c97071
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 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.camera.video
+
+import android.content.Context
+import android.os.Build
+import android.os.Looper
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakeAppConfig
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import java.io.File
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP
+)
+class VideoCaptureLegacyTest {
+    @Before
+    fun setUp() {
+        val camera = FakeCamera()
+
+        val cameraFactoryProvider =
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+                val cameraFactory = FakeCameraFactory()
+                cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
+                    camera
+                }
+                cameraFactory
+            }
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
+            .setCameraFactoryProvider(cameraFactoryProvider)
+            .build()
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        CameraX.initialize(context, cameraXConfig).get()
+    }
+
+    @After
+    fun tearDown() {
+        CameraX.shutdown().get()
+    }
+
+    @Test
+    fun startRecording_beforeUseCaseIsBound() {
+        val videoCaptureLegacy = VideoCaptureLegacy.Builder().build()
+        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        val outputFileOptions = VideoCaptureLegacy.OutputFileOptions.Builder(file).build()
+        val callback = mock(VideoCaptureLegacy.OnVideoSavedCallback::class.java)
+        videoCaptureLegacy.startRecording(
+            outputFileOptions,
+            CameraXExecutors.mainThreadExecutor(),
+            callback
+        )
+        shadowOf(Looper.getMainLooper()).idle()
+
+        verify(callback).onError(eq(VideoCaptureLegacy.ERROR_INVALID_CAMERA), anyString(), any())
+    }
+}
\ No newline at end of file