/* * Copyright 2021 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.Manifest; import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.camera.core.impl.utils.ContextUtil; import androidx.core.content.PermissionChecker; import androidx.core.util.Consumer; import androidx.core.util.Preconditions; import java.util.concurrent.Executor; /** * A recording that can be started at a future time. * *

A pending recording allows for configuration of a recording before it is started. Once a * pending recording is started with {@link #start(Executor, Consumer)}, any changes to the pending * recording will not affect the actual recording; any modifications to the recording will need * to occur through the controls of the {@link SucklessRecording} class returned by * {@link #start(Executor, Consumer)}. * *

A pending recording can be created using one of the {@link Recorder} methods for starting a * recording such as {@link Recorder#prepareRecording(Context, MediaStoreOutputOptions)}. *

There may be more settings that can only be changed per-recorder instead of per-recording, * because it requires expensive operations like reconfiguring the camera. For those settings, use * the {@link Recorder.Builder} methods to configure before creating the {@link Recorder} * instance, then create the pending recording with it. */ @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java @SuppressLint("RestrictedApi") public final class SucklessPendingRecording { private final Context mContext; private final SucklessRecorder mRecorder; private final OutputOptions mOutputOptions; private Consumer mEventListener; private Executor mListenerExecutor; private boolean mAudioEnabled = false; private boolean mIsPersistent = false; SucklessPendingRecording(@NonNull Context context, @NonNull SucklessRecorder recorder, @NonNull OutputOptions options) { // Application context is sufficient for all our needs, so store that to avoid leaking // unused resources. For attribution, ContextUtil.getApplicationContext() will retain the // attribution tag from the original context. mContext = ContextUtil.getApplicationContext(context); mRecorder = recorder; mOutputOptions = options; } /** * Returns an application context which was retrieved from the {@link Context} used to * create this object. */ @NonNull Context getApplicationContext() { return mContext; } @NonNull SucklessRecorder getRecorder() { return mRecorder; } @NonNull OutputOptions getOutputOptions() { return mOutputOptions; } @Nullable Executor getListenerExecutor() { return mListenerExecutor; } @Nullable Consumer getEventListener() { return mEventListener; } boolean isAudioEnabled() { return mAudioEnabled; } boolean isPersistent() { return mIsPersistent; } /** * Enables audio to be recorded for this recording. * *

This method must be called prior to {@link #start(Executor, Consumer)} to enable audio * in the recording. If this method is not called, the {@link SucklessRecording} generated by * {@link #start(Executor, Consumer)} will not contain audio, and * {@link AudioStats#getAudioState()} will always return * {@link AudioStats#AUDIO_STATE_DISABLED} for all {@link RecordingStats} send to the listener * set passed to {@link #start(Executor, Consumer)}. * *

Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO} * permission; without it, recording will fail at {@link #start(Executor, Consumer)} with an * {@link IllegalStateException}. * * @return this pending recording * @throws IllegalStateException if the {@link Recorder} this recording is associated to * doesn't support audio. * @throws SecurityException if the {@link Manifest.permission#RECORD_AUDIO} permission * is denied for the current application. */ @RequiresPermission(Manifest.permission.RECORD_AUDIO) @NonNull public SucklessPendingRecording withAudioEnabled() { // Check permissions and throw a security exception if RECORD_AUDIO is not granted. if (PermissionChecker.checkSelfPermission(mContext, Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_DENIED) { throw new SecurityException("Attempted to enable audio for recording but application " + "does not have RECORD_AUDIO permission granted."); } Preconditions.checkState(mRecorder.isAudioSupported(), "The Recorder this recording is " + "associated to doesn't support audio."); mAudioEnabled = true; return this; } /** * Configures the recording to be a persistent recording. * *

A persistent recording will only be stopped by explicitly calling * {@link Recording#stop()} or {@link Recording#close()} and will ignore events that would * normally cause recording to stop, such as lifecycle events or explicit unbinding of a * {@link VideoCapture} use case that the recording's {@link Recorder} is attached to. * *

Even though lifecycle events or explicit unbinding use cases won't stop a persistent * recording, it will still stop the camera from producing data, resulting in the in-progress * persistent recording stopping getting data until the camera stream is activated again. For * example, when the activity goes into background, the recording will keep waiting for new * data to be recorded until the activity is back to foreground. * *

A {@link Recorder} instance is recommended to be associated with a single * {@link VideoCapture} instance, especially when using persistent recording. Otherwise, there * might be unexpected behavior. Any in-progress persistent recording created from the same * {@link Recorder} should be stopped before starting a new recording, even if the * {@link Recorder} is associated with a different {@link VideoCapture}. * *

To switch to a different camera stream while a recording is in progress, first create * the recording as persistent recording, then rebind the {@link VideoCapture} it's * associated with to a different camera. The implementation may be like: *

{@code
     * // Prepare the Recorder and VideoCapture, then bind the VideoCapture to the back camera.
     * Recorder recorder = Recorder.Builder().build();
     * VideoCapture videoCapture = VideoCapture.withOutput(recorder);
     * cameraProvider.bindToLifecycle(
     *         lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, videoCapture);
     *
     * // Prepare the persistent recording and start it.
     * Recording recording = recorder
     *         .prepareRecording(context, outputOptions)
     *         .asPersistentRecording()
     *         .start(eventExecutor, eventListener);
     *
     * // Record from the back camera for a period of time.
     *
     * // Rebind the VideoCapture to the front camera.
     * cameraProvider.unbindAll();
     * cameraProvider.bindToLifecycle(
     *         lifecycleOwner, CameraSelector.DEFAULT_FRONT_CAMERA, videoCapture);
     *
     * // Record from the front camera for a period of time.
     *
     * // Stop the recording explicitly.
     * recording.stop();
     * }
* *

The audio data will still be recorded after the {@link VideoCapture} is unbound. * {@link Recording#pause() Pause} the recording first and {@link Recording#resume() resume} it * later to stop recording audio while rebinding use cases. * *

If the recording is unable to receive data from the new camera, possibly because of * incompatible surface combination, an exception will be thrown when binding to lifecycle. */ @ExperimentalPersistentRecording @NonNull public SucklessPendingRecording asPersistentRecording() { mIsPersistent = true; return this; } /** * Starts the recording, making it an active recording. * *

Only a single recording can be active at a time, so if another recording is active, * this will throw an {@link IllegalStateException}. * *

If there are no errors starting the recording, the returned {@link SucklessRecording} * can be used to {@link SucklessRecording#pause() pause}, {@link SucklessRecording#resume() resume}, * or {@link SucklessRecording#stop() stop} the recording. * *

Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will * be the first event sent to the provided event listener. * *

If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event * will be the first event sent to the provided listener, and information about the error can * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method. The returned * {@link SucklessRecording} will be in a finalized state, and all controls will be no-ops. * *

If the returned {@link SucklessRecording} is garbage collected, the recording will be * automatically stopped. A reference to the active recording must be maintained as long as * the recording needs to be active. If the recording is garbage collected, the * {@link VideoRecordEvent.Finalize} event will contain error * {@link VideoRecordEvent.Finalize#ERROR_RECORDING_GARBAGE_COLLECTED}. * *

The {@link Recording} will be stopped automatically if the {@link VideoCapture} its * {@link Recorder} is attached to is unbound unless it's created * {@link #asPersistentRecording() as a persistent recording}. * * @throws IllegalStateException if the associated Recorder currently has an unfinished * active recording. * @param listenerExecutor the executor that the event listener will be run on. * @param listener the event listener to handle video record events. */ @NonNull @CheckResult public SucklessRecording start( @NonNull Executor listenerExecutor, @NonNull Consumer listener) { Preconditions.checkNotNull(listenerExecutor, "Listener Executor can't be null."); Preconditions.checkNotNull(listener, "Event listener can't be null"); mListenerExecutor = listenerExecutor; mEventListener = listener; return mRecorder.start(this); } }