[Video] Expose API for setting location metadata
* Add API setLocation() on OutputOptions'
* Apply the location of OutputOptions to the MediaMuxer.setLocation() in Recorder
This CL also refactor the OutputOptions.
* Move the common get/set methods from extended OutputOptions classes to OutputOptions/OutputOptions.Builder, for example move the implementation of get/setFileSizeLimit from FileOutputOptions to OutputOptions. This prevent from the duplicate code for having a common property.
Relnote: "Exposed API for setting location metadata to the saved video. An android.location.Location object can be set via new API androidx.camera.video.OutputOptions.Builder.setLocation(Location)."
Bug: 204197544
Test: ./gradlew camera:camera-video:connectedAndroidTest
Change-Id: I313a013445776ed730d6f98f57113e4998306e86
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index 94e5745..f8a2ad5 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -20,7 +20,6 @@
}
@RequiresApi(21) public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
- method public long getFileSizeLimit();
method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
}
@@ -28,24 +27,24 @@
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
method public java.io.File getFile();
- method public long getFileSizeLimit();
}
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
method public android.net.Uri getCollectionUri();
method public android.content.ContentResolver getContentResolver();
method public android.content.ContentValues getContentValues();
- method public long getFileSizeLimit();
field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
}
@@ -54,10 +53,12 @@
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method public abstract long getFileSizeLimit();
+ method public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
}
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index 94e5745..f8a2ad5 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -20,7 +20,6 @@
}
@RequiresApi(21) public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
- method public long getFileSizeLimit();
method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
}
@@ -28,24 +27,24 @@
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
method public java.io.File getFile();
- method public long getFileSizeLimit();
}
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
method public android.net.Uri getCollectionUri();
method public android.content.ContentResolver getContentResolver();
method public android.content.ContentValues getContentValues();
- method public long getFileSizeLimit();
field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
}
@@ -54,10 +53,12 @@
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method public abstract long getFileSizeLimit();
+ method public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
}
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index 94e5745..f8a2ad5 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -20,7 +20,6 @@
}
@RequiresApi(21) public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
- method public long getFileSizeLimit();
method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
}
@@ -28,24 +27,24 @@
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
method public java.io.File getFile();
- method public long getFileSizeLimit();
}
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
method public android.net.Uri getCollectionUri();
method public android.content.ContentResolver getContentResolver();
method public android.content.ContentValues getContentValues();
- method public long getFileSizeLimit();
field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
}
@@ -54,10 +53,12 @@
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method public abstract long getFileSizeLimit();
+ method public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
}
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index b4e94bf..8a3a750 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -58,6 +58,7 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.autoValueAnnotations)
androidTestImplementation(project(":camera:camera-lifecycle"))
androidTestImplementation(project(":camera:camera-testing"))
androidTestImplementation(libs.kotlinStdlib)
@@ -67,6 +68,7 @@
androidTestImplementation libs.mockitoKotlin, {
exclude group: 'org.mockito' // to keep control on the mockito version
}
+ androidTestAnnotationProcessor(libs.autoValue)
}
android {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/FakeOutputOptions.java b/camera/camera-video/src/androidTest/java/androidx/camera/video/FakeOutputOptions.java
new file mode 100644
index 0000000..06cf635
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/FakeOutputOptions.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 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 androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+/** A fake implementation of {@link OutputOptions}. */
+// Java is used because @AutoValue is required.
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class FakeOutputOptions extends OutputOptions {
+
+ private FakeOutputOptions(@NonNull FakeOutputOptionsInternal fakeOutputOptionsInternal) {
+ super(fakeOutputOptionsInternal);
+ }
+
+ /** The builder of the {@link FakeOutputOptions} object. */
+ @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+ public static final class Builder extends OutputOptions.Builder<FakeOutputOptions, Builder> {
+
+ /** Creates a builder of the {@link FakeOutputOptions}. */
+ public Builder() {
+ super(new AutoValue_FakeOutputOptions_FakeOutputOptionsInternal.Builder());
+ }
+
+ /** Builds the {@link FakeOutputOptions} instance. */
+ @Override
+ @NonNull
+ public FakeOutputOptions build() {
+ return new FakeOutputOptions(
+ ((FakeOutputOptionsInternal.Builder) mRootInternalBuilder).build());
+ }
+ }
+
+ @AutoValue
+ abstract static class FakeOutputOptionsInternal extends OutputOptions.OutputOptionsInternal {
+
+ @AutoValue.Builder
+ abstract static class Builder extends OutputOptions.OutputOptionsInternal.Builder<Builder> {
+
+ @SuppressWarnings("NullableProblems") // Nullable problem in AutoValue generated class
+ @Override
+ @NonNull
+ abstract FakeOutputOptionsInternal build();
+ }
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
index c221b5b..9c73beb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
@@ -19,13 +19,16 @@
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
+import android.location.Location
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
@@ -136,4 +139,52 @@
}
savedFile.delete()
}
+
+ @Test
+ fun defaultLocationIsNull() {
+ val outputOptions = FakeOutputOptions.Builder().build()
+
+ assertThat(outputOptions.location).isNull()
+ }
+
+ @Test
+ fun setValidLocation() {
+ listOf(
+ createLocation(0.0, 0.0),
+ createLocation(90.0, 180.0),
+ createLocation(-90.0, -180.0),
+ createLocation(10.1234, -100.5678),
+ ).forEach { location ->
+ val outputOptions = FakeOutputOptions.Builder().setLocation(location).build()
+
+ assertWithMessage("Test $location failed")
+ .that(outputOptions.location).isEqualTo(location)
+ }
+ }
+
+ @Test
+ fun setInvalidLocation() {
+ listOf(
+ createLocation(Double.NaN, 0.0),
+ createLocation(0.0, Double.NaN),
+ createLocation(90.5, 0.0),
+ createLocation(-90.5, 0.0),
+ createLocation(0.0, 180.5),
+ createLocation(0.0, -180.5),
+ ).forEach { location ->
+ assertThrows(IllegalArgumentException::class.java) {
+ FakeOutputOptions.Builder().setLocation(location)
+ }
+ }
+ }
+
+ private fun createLocation(
+ latitude: Double,
+ longitude: Double,
+ provider: String = "FakeProvider"
+ ): Location =
+ Location(provider).apply {
+ this.latitude = latitude
+ this.longitude = longitude
+ }
}
\ No newline at end of file
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index a1ffe7e..3d62333 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -26,6 +26,7 @@
import android.content.ContentValues
import android.content.Context
import android.graphics.SurfaceTexture
+import android.location.Location
import android.media.MediaMetadataRetriever
import android.media.MediaRecorder
import android.net.Uri
@@ -66,6 +67,7 @@
import androidx.testutils.assertThrows
import androidx.testutils.fail
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import java.io.File
import java.util.concurrent.Executor
import java.util.concurrent.Semaphore
@@ -575,6 +577,14 @@
}
@Test
+ fun setLocation() {
+ // TODO(leohuang): add a test to verify negative latitude and longitude.
+ // MediaMuxer.setLocation() causes a little loss of precision on negative values.
+ // See b/232327925.
+ runLocationTest(createLocation(25.033267462243586, 121.56454121737946))
+ }
+
+ @Test
fun checkStreamState() {
clearInvocations(videoRecordEventListener)
invokeSurfaceRequest()
@@ -1176,34 +1186,69 @@
}
private fun checkFileAudio(uri: Uri, hasAudio: Boolean) {
- val mediaRetriever = MediaMetadataRetriever()
- mediaRetriever.apply {
- setDataSource(context, uri)
- val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
+ MediaMetadataRetriever().apply {
+ try {
+ setDataSource(context, uri)
+ val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
- assertThat(value).isEqualTo(
- if (hasAudio) {
- "yes"
- } else {
- null
- }
- )
+ assertThat(value).isEqualTo(
+ if (hasAudio) {
+ "yes"
+ } else {
+ null
+ }
+ )
+ } finally {
+ release()
+ }
}
}
private fun checkFileVideo(uri: Uri, hasVideo: Boolean) {
- val mediaRetriever = MediaMetadataRetriever()
- mediaRetriever.apply {
- setDataSource(context, uri)
- val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+ MediaMetadataRetriever().apply {
+ try {
+ setDataSource(context, uri)
+ val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
- assertThat(value).isEqualTo(
- if (hasVideo) {
- "yes"
- } else {
- null
- }
- )
+ assertThat(value).isEqualTo(
+ if (hasVideo) {
+ "yes"
+ } else {
+ null
+ }
+ )
+ } finally {
+ release()
+ }
+ }
+ }
+
+ private fun checkLocation(uri: Uri, location: Location) {
+ MediaMetadataRetriever().apply {
+ try {
+ setDataSource(context, uri)
+ // Only test on mp4 output format, others will be ignored.
+ val mime = extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
+ assumeTrue("Unsupported mime = $mime",
+ "video/mp4".equals(mime, ignoreCase = true))
+ val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
+ assertThat(value).isNotNull()
+ // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
+ val matchGroup =
+ "([\\+-]?[0-9]+(\\.[0-9]+)?)([\\+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
+ .find(value!!) ?: fail("Fail on checking location metadata: $value")
+ val lat = matchGroup.groupValues[1].toDouble()
+ val lon = matchGroup.groupValues[3].toDouble()
+
+ // MediaMuxer.setLocation rounds the value to 4 decimal places
+ val tolerance = 0.0001
+ assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
+ .that(lat).isWithin(tolerance).of(location.latitude)
+ assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
+ .that(lon).isWithin(tolerance).of(location.longitude)
+ } finally {
+ release()
+ }
}
}
@@ -1256,4 +1301,43 @@
recording.close()
file.delete()
}
+
+ private fun runLocationTest(location: Location) {
+ val recorder = Recorder.Builder().build()
+ invokeSurfaceRequest(recorder)
+ val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ val outputOptions = FileOutputOptions.Builder(file)
+ .setLocation(location)
+ .build()
+
+ val recording = recorder
+ .prepareRecording(context, outputOptions)
+ .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
+
+ val inOrder = inOrder(videoRecordEventListener)
+ inOrder.verify(videoRecordEventListener, timeout(5000L))
+ .accept(any(VideoRecordEvent.Start::class.java))
+ inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
+ .accept(any(VideoRecordEvent.Status::class.java))
+
+ recording.stopSafely()
+
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
+ .accept(any(VideoRecordEvent.Finalize::class.java))
+
+ val uri = Uri.fromFile(file)
+ checkLocation(uri, location)
+
+ file.delete()
+ }
+
+ private fun createLocation(
+ latitude: Double,
+ longitude: Double,
+ provider: String = "FakeProvider"
+ ): Location =
+ Location(provider).apply {
+ this.latitude = latitude
+ this.longitude = longitude
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
index 7c69be4..495ca67 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
@@ -43,8 +43,7 @@
FileDescriptorOutputOptions(
@NonNull FileDescriptorOutputOptionsInternal fileDescriptorOutputOptionsInternal) {
- Preconditions.checkNotNull(fileDescriptorOutputOptionsInternal,
- "FileDescriptorOutputOptionsInternal can't be null.");
+ super(fileDescriptorOutputOptionsInternal);
mFileDescriptorOutputOptionsInternal = fileDescriptorOutputOptionsInternal;
}
@@ -58,14 +57,6 @@
return mFileDescriptorOutputOptionsInternal.getParcelFileDescriptor();
}
- /**
- * Gets the limit for the file length in bytes.
- */
- @Override
- public long getFileSizeLimit() {
- return mFileDescriptorOutputOptionsInternal.getFileSizeLimit();
- }
-
@Override
@NonNull
public String toString() {
@@ -93,12 +84,10 @@
/** The builder of the {@link FileDescriptorOutputOptions} object. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
- public static final class Builder implements
+ public static final class Builder extends
OutputOptions.Builder<FileDescriptorOutputOptions, Builder> {
- private final FileDescriptorOutputOptionsInternal.Builder mInternalBuilder =
- new AutoValue_FileDescriptorOutputOptions_FileDescriptorOutputOptionsInternal
- .Builder()
- .setFileSizeLimit(FILE_SIZE_UNLIMITED);
+
+ private final FileDescriptorOutputOptionsInternal.Builder mInternalBuilder;
/**
* Creates a builder of the {@link FileDescriptorOutputOptions} with a file descriptor.
@@ -106,7 +95,10 @@
* @param fileDescriptor the file descriptor to use as the output destination.
*/
public Builder(@NonNull ParcelFileDescriptor fileDescriptor) {
+ super(new AutoValue_FileDescriptorOutputOptions_FileDescriptorOutputOptionsInternal
+ .Builder());
Preconditions.checkNotNull(fileDescriptor, "File descriptor can't be null.");
+ mInternalBuilder = (FileDescriptorOutputOptionsInternal.Builder) mRootInternalBuilder;
mInternalBuilder.setParcelFileDescriptor(fileDescriptor);
}
@@ -124,8 +116,7 @@
@Override
@NonNull
public Builder setFileSizeLimit(long fileSizeLimitBytes) {
- mInternalBuilder.setFileSizeLimit(fileSizeLimitBytes);
- return this;
+ return super.setFileSizeLimit(fileSizeLimitBytes);
}
/** Builds the {@link FileDescriptorOutputOptions} instance. */
@@ -137,18 +128,17 @@
}
@AutoValue
- abstract static class FileDescriptorOutputOptionsInternal {
+ abstract static class FileDescriptorOutputOptionsInternal extends OutputOptionsInternal {
@NonNull
abstract ParcelFileDescriptor getParcelFileDescriptor();
- abstract long getFileSizeLimit();
+ @SuppressWarnings("NullableProblems") // Nullable problem in AutoValue generated class
@AutoValue.Builder
- abstract static class Builder {
+ abstract static class Builder extends OutputOptionsInternal.Builder<Builder> {
@NonNull
abstract Builder setParcelFileDescriptor(
@NonNull ParcelFileDescriptor parcelFileDescriptor);
- @NonNull
- abstract Builder setFileSizeLimit(long fileSizeLimitBytes);
+ @Override
@NonNull
abstract FileDescriptorOutputOptionsInternal build();
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
index 131a458..9bff991 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
@@ -39,8 +39,7 @@
private final FileOutputOptionsInternal mFileOutputOptionsInternal;
FileOutputOptions(@NonNull FileOutputOptionsInternal fileOutputOptionsInternal) {
- Preconditions.checkNotNull(fileOutputOptionsInternal,
- "FileOutputOptionsInternal can't be null.");
+ super(fileOutputOptionsInternal);
mFileOutputOptionsInternal = fileOutputOptionsInternal;
}
@@ -50,14 +49,6 @@
return mFileOutputOptionsInternal.getFile();
}
- /**
- * Gets the limit for the file length in bytes.
- */
- @Override
- public long getFileSizeLimit() {
- return mFileOutputOptionsInternal.getFileSizeLimit();
- }
-
@Override
@NonNull
public String toString() {
@@ -85,10 +76,9 @@
/** The builder of the {@link FileOutputOptions} object. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
- public static final class Builder implements OutputOptions.Builder<FileOutputOptions, Builder> {
- private final FileOutputOptionsInternal.Builder mInternalBuilder =
- new AutoValue_FileOutputOptions_FileOutputOptionsInternal.Builder()
- .setFileSizeLimit(OutputOptions.FILE_SIZE_UNLIMITED);
+ public static final class Builder extends OutputOptions.Builder<FileOutputOptions, Builder> {
+
+ private final FileOutputOptionsInternal.Builder mInternalBuilder;
/**
* Creates a builder of the {@link FileOutputOptions} with a file object.
@@ -101,7 +91,9 @@
*/
@SuppressWarnings("StreamFiles") // FileDescriptor API is in FileDescriptorOutputOptions
public Builder(@NonNull File file) {
+ super(new AutoValue_FileOutputOptions_FileOutputOptionsInternal.Builder());
Preconditions.checkNotNull(file, "File can't be null.");
+ mInternalBuilder = (FileOutputOptionsInternal.Builder) mRootInternalBuilder;
mInternalBuilder.setFile(file);
}
@@ -119,8 +111,7 @@
@Override
@NonNull
public Builder setFileSizeLimit(long fileSizeLimitBytes) {
- mInternalBuilder.setFileSizeLimit(fileSizeLimitBytes);
- return this;
+ return super.setFileSizeLimit(fileSizeLimitBytes);
}
/** Builds the {@link FileOutputOptions} instance. */
@@ -132,20 +123,19 @@
}
@AutoValue
- abstract static class FileOutputOptionsInternal {
+ abstract static class FileOutputOptionsInternal extends OutputOptions.OutputOptionsInternal {
+
@NonNull
abstract File getFile();
- abstract long getFileSizeLimit();
-
+ @SuppressWarnings("NullableProblems") // Nullable problem in AutoValue generated class
@AutoValue.Builder
- abstract static class Builder {
+ abstract static class Builder extends OutputOptions.OutputOptionsInternal.Builder<Builder> {
+
@NonNull
abstract Builder setFile(@NonNull File file);
- @NonNull
- abstract Builder setFileSizeLimit(long fileSizeLimitBytes);
-
+ @Override
@NonNull
abstract FileOutputOptionsInternal build();
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
index 63d49ee..ebe74cd 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
@@ -70,8 +70,7 @@
MediaStoreOutputOptions(
@NonNull MediaStoreOutputOptionsInternal mediaStoreOutputOptionsInternal) {
- Preconditions.checkNotNull(mediaStoreOutputOptionsInternal,
- "MediaStoreOutputOptionsInternal can't be null.");
+ super(mediaStoreOutputOptionsInternal);
mMediaStoreOutputOptionsInternal = mediaStoreOutputOptionsInternal;
}
@@ -105,16 +104,6 @@
return mMediaStoreOutputOptionsInternal.getContentValues();
}
- /**
- * Gets the limit for the file length in bytes.
- *
- * @see Builder#setFileSizeLimit(long)
- */
- @Override
- public long getFileSizeLimit() {
- return mMediaStoreOutputOptionsInternal.getFileSizeLimit();
- }
-
@Override
@NonNull
public String toString() {
@@ -141,12 +130,10 @@
}
/** The builder of the {@link MediaStoreOutputOptions} object. */
- public static final class Builder implements
+ public static final class Builder extends
OutputOptions.Builder<MediaStoreOutputOptions, Builder> {
- private final MediaStoreOutputOptionsInternal.Builder mInternalBuilder =
- new AutoValue_MediaStoreOutputOptions_MediaStoreOutputOptionsInternal.Builder()
- .setContentValues(EMPTY_CONTENT_VALUES)
- .setFileSizeLimit(FILE_SIZE_UNLIMITED);
+
+ private final MediaStoreOutputOptionsInternal.Builder mInternalBuilder;
/**
* Creates a builder of the {@link MediaStoreOutputOptions} with media store options.
@@ -171,9 +158,13 @@
* @param collectionUri the URI of the collection to insert into.
*/
public Builder(@NonNull ContentResolver contentResolver, @NonNull Uri collectionUri) {
+ super(new AutoValue_MediaStoreOutputOptions_MediaStoreOutputOptionsInternal.Builder());
Preconditions.checkNotNull(contentResolver, "Content resolver can't be null.");
Preconditions.checkNotNull(collectionUri, "Collection Uri can't be null.");
- mInternalBuilder.setContentResolver(contentResolver).setCollectionUri(collectionUri);
+ mInternalBuilder = (MediaStoreOutputOptionsInternal.Builder) mRootInternalBuilder;
+ mInternalBuilder.setContentResolver(contentResolver)
+ .setCollectionUri(collectionUri)
+ .setContentValues(new ContentValues());
}
/**
@@ -211,8 +202,7 @@
@Override
@NonNull
public Builder setFileSizeLimit(long fileSizeLimitBytes) {
- mInternalBuilder.setFileSizeLimit(fileSizeLimitBytes);
- return this;
+ return super.setFileSizeLimit(fileSizeLimitBytes);
}
/** Builds the {@link MediaStoreOutputOptions} instance. */
@@ -224,25 +214,24 @@
}
@AutoValue
- abstract static class MediaStoreOutputOptionsInternal {
+ abstract static class MediaStoreOutputOptionsInternal extends OutputOptionsInternal {
@NonNull
abstract ContentResolver getContentResolver();
@NonNull
abstract Uri getCollectionUri();
@NonNull
abstract ContentValues getContentValues();
- abstract long getFileSizeLimit();
+ @SuppressWarnings("NullableProblems") // Nullable problem in AutoValue generated class
@AutoValue.Builder
- abstract static class Builder {
+ abstract static class Builder extends OutputOptionsInternal.Builder<Builder> {
@NonNull
abstract Builder setContentResolver(@NonNull ContentResolver contentResolver);
@NonNull
abstract Builder setCollectionUri(@NonNull Uri collectionUri);
@NonNull
abstract Builder setContentValues(@NonNull ContentValues contentValues);
- @NonNull
- abstract Builder setFileSizeLimit(long fileSizeLimitBytes);
+ @Override
@NonNull
abstract MediaStoreOutputOptionsInternal build();
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
index c5c113d..3b6f924 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
@@ -16,8 +16,12 @@
package androidx.camera.video;
+import android.location.Location;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
/**
* Options for configuring output destination for generating a recording.
@@ -36,7 +40,10 @@
/** Represents an unbound file size. */
public static final int FILE_SIZE_UNLIMITED = 0;
- OutputOptions() {
+ private final OutputOptionsInternal mOutputOptionsInternal;
+
+ OutputOptions(@NonNull OutputOptionsInternal outputOptionsInternal) {
+ mOutputOptionsInternal = outputOptionsInternal;
}
/**
@@ -44,12 +51,34 @@
*
* @return the file size limit in bytes.
*/
- public abstract long getFileSizeLimit();
+ public long getFileSizeLimit() {
+ return mOutputOptionsInternal.getFileSizeLimit();
+ }
+
+ /**
+ * Returns a {@link Location} object representing the geographic location where the video was
+ * recorded.
+ *
+ * @return The location object or {@code null} if no location was set.
+ */
+ @Nullable
+ public Location getLocation() {
+ return mOutputOptionsInternal.getLocation();
+ }
/**
* The builder of the {@link OutputOptions}.
*/
- interface Builder<T extends OutputOptions, B> {
+ @SuppressWarnings("unchecked") // Cast to type B
+ abstract static class Builder<T extends OutputOptions, B> {
+
+ final OutputOptionsInternal.Builder<?> mRootInternalBuilder;
+
+ Builder(@NonNull OutputOptionsInternal.Builder<?> builder) {
+ mRootInternalBuilder = builder;
+ // Apply default value
+ mRootInternalBuilder.setFileSizeLimit(FILE_SIZE_UNLIMITED);
+ }
/**
* Sets the limit for the file length in bytes.
@@ -57,12 +86,66 @@
* <p>If not set, defaults to {@link #FILE_SIZE_UNLIMITED}.
*/
@NonNull
- B setFileSizeLimit(long bytes);
+ public B setFileSizeLimit(long bytes) {
+ mRootInternalBuilder.setFileSizeLimit(bytes);
+ return (B) this;
+ }
+
+ /**
+ * Sets a {@link Location} object representing a geographic location where the video was
+ * recorded.
+ *
+ * <p>When use with {@link Recorder}, the geographic location is stored in udta box if the
+ * output format is MP4, and is ignored for other formats. The geographic location is
+ * stored according to ISO-6709 standard.
+ *
+ * <p>If {@code null}, no location information will be saved with the video. Default
+ * value is {@code null}.
+ *
+ * @throws IllegalArgumentException if the latitude of the location is not in the range
+ * [-90, 90] or the longitude of the location is not in the range [-180, 180].
+ */
+ @NonNull
+ public B setLocation(@Nullable Location location) {
+ if (location != null) {
+ Preconditions.checkArgument(
+ location.getLatitude() >= -90 && location.getLatitude() <= 90,
+ "Latitude must be in the range [-90, 90]");
+ Preconditions.checkArgument(
+ location.getLongitude() >= -180 && location.getLongitude() <= 180,
+ "Longitude must be in the range [-180, 180]");
+ }
+ mRootInternalBuilder.setLocation(location);
+ return (B) this;
+ }
/**
* Builds the {@link OutputOptions} instance.
*/
@NonNull
- T build();
+ abstract T build();
+ }
+
+ // A base class of a @AutoValue class
+ abstract static class OutputOptionsInternal {
+
+ abstract long getFileSizeLimit();
+
+ @Nullable
+ abstract Location getLocation();
+
+ // A base class of a @AutoValue.Builder class
+ @SuppressWarnings("NullableProblems") // Nullable problem in AutoValue generated class
+ abstract static class Builder<B> {
+
+ @NonNull
+ abstract B setFileSizeLimit(long fileSizeLimitBytes);
+
+ @NonNull
+ abstract B setLocation(@Nullable Location location);
+
+ @NonNull
+ abstract OutputOptionsInternal build();
+ }
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index 66cd575..5cb6438 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -31,6 +31,7 @@
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
+import android.location.Location;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.media.MediaScannerConnection;
@@ -1512,6 +1513,7 @@
return;
}
+ MediaMuxer mediaMuxer;
try {
MediaSpec mediaSpec = getObservableData(mMediaSpec);
int muxerOutputFormat =
@@ -1520,7 +1522,7 @@
MediaSpec.outputFormatToMuxerFormat(
MEDIA_SPEC_DEFAULT.getOutputFormat()))
: MediaSpec.outputFormatToMuxerFormat(mediaSpec.getOutputFormat());
- mMediaMuxer = recordingToStart.performOneTimeMediaMuxerCreation(muxerOutputFormat,
+ mediaMuxer = recordingToStart.performOneTimeMediaMuxerCreation(muxerOutputFormat,
uri -> mOutputUri = uri);
} catch (IOException e) {
onInProgressRecordingInternalError(recordingToStart, ERROR_INVALID_OUTPUT_OPTIONS,
@@ -1528,16 +1530,30 @@
return;
}
- // TODO: Add more metadata to MediaMuxer, e.g. location information.
if (mSurfaceTransformationInfo != null) {
- mMediaMuxer.setOrientationHint(mSurfaceTransformationInfo.getRotationDegrees());
+ mediaMuxer.setOrientationHint(mSurfaceTransformationInfo.getRotationDegrees());
+ }
+ Location location = recordingToStart.getOutputOptions().getLocation();
+ if (location != null) {
+ try {
+ mediaMuxer.setLocation((float) location.getLatitude(),
+ (float) location.getLongitude());
+ } catch (IllegalArgumentException e) {
+ mediaMuxer.release();
+ onInProgressRecordingInternalError(recordingToStart,
+ ERROR_INVALID_OUTPUT_OPTIONS, e);
+ return;
+ }
}
- mVideoTrackIndex = mMediaMuxer.addTrack(mVideoOutputConfig.getMediaFormat());
+ mVideoTrackIndex = mediaMuxer.addTrack(mVideoOutputConfig.getMediaFormat());
if (isAudioEnabled()) {
- mAudioTrackIndex = mMediaMuxer.addTrack(mAudioOutputConfig.getMediaFormat());
+ mAudioTrackIndex = mediaMuxer.addTrack(mAudioOutputConfig.getMediaFormat());
}
- mMediaMuxer.start();
+ mediaMuxer.start();
+
+ // MediaMuxer is successfully initialized, transfer the ownership to Recorder.
+ mMediaMuxer = mediaMuxer;
// Write first data to ensure tracks are not empty
writeVideoData(videoDataToWrite, recordingToStart);