diff --git a/build.gradle.kts b/build.gradle.kts index 331c0dd2..09521f1b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:7.0.3") + classpath("com.android.tools.build:gradle:7.3.1") classpath("io.deepmedia.tools:publisher:0.6.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") diff --git a/cameraview/build.gradle.kts b/cameraview/build.gradle.kts index 406d693a..745c04d2 100644 --- a/cameraview/build.gradle.kts +++ b/cameraview/build.gradle.kts @@ -19,6 +19,7 @@ android { "com.otaliastudios.cameraview.tools.SdkExcludeFilter," + "com.otaliastudios.cameraview.tools.SdkIncludeFilter" } + namespace = "com.otaliastudios.cameraview" buildTypes["debug"].isTestCoverageEnabled = true buildTypes["release"].isMinifyEnabled = false } diff --git a/cameraview/src/androidTest/AndroidManifest.xml b/cameraview/src/androidTest/AndroidManifest.xml index 431ee7a8..ac5c2540 100644 --- a/cameraview/src/androidTest/AndroidManifest.xml +++ b/cameraview/src/androidTest/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/cameraview/src/main/AndroidManifest.xml b/cameraview/src/main/AndroidManifest.xml index ce05aa56..95887d2f 100644 --- a/cameraview/src/main/AndroidManifest.xml +++ b/cameraview/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index f5140eb2..7cc99e04 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -595,18 +595,18 @@ public boolean mapGesture(@NonNull Gesture gesture, @NonNull GestureAction actio mPinchGestureFinder.setActive(mGestureMap.get(Gesture.PINCH) != none); break; case TAP: - // case DOUBLE_TAP: + // case DOUBLE_TAP: case LONG_TAP: mTapGestureFinder.setActive( mGestureMap.get(Gesture.TAP) != none || - // mGestureMap.get(Gesture.DOUBLE_TAP) != none || - mGestureMap.get(Gesture.LONG_TAP) != none); + // mGestureMap.get(Gesture.DOUBLE_TAP) != none || + mGestureMap.get(Gesture.LONG_TAP) != none); break; case SCROLL_HORIZONTAL: case SCROLL_VERTICAL: mScrollGestureFinder.setActive( mGestureMap.get(Gesture.SCROLL_HORIZONTAL) != none || - mGestureMap.get(Gesture.SCROLL_VERTICAL) != none); + mGestureMap.get(Gesture.SCROLL_VERTICAL) != none); break; } @@ -1780,6 +1780,25 @@ public void run() { } }); } + /** + * Starts recording a fast, low quality video snapshot. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * + * @param file a file where the video will be saved + */ + public void takeVideoSnapshot(@NonNull File file) { + takeVideoSnapshot(file, null); + } + + /** + * Starts recording a fast, low quality video snapshot. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * + * @param fileDescriptor a file descriptor where the video will be saved + */ + public void takeVideoSnapshot(@NonNull FileDescriptor fileDescriptor) { + takeVideoSnapshot(null, fileDescriptor); + } /** * Starts recording a fast, low quality video snapshot. Video will be written to the given file, @@ -1790,9 +1809,15 @@ public void run() { * * @param file a file where the video will be saved */ - public void takeVideoSnapshot(@NonNull File file) { + public void takeVideoSnapshot(@Nullable File file, @Nullable FileDescriptor fileDescriptor) { VideoResult.Stub stub = new VideoResult.Stub(); - mCameraEngine.takeVideoSnapshot(stub, file); + if (file != null) { + mCameraEngine.takeVideoSnapshot(stub, file, null); + } else if (fileDescriptor != null) { + mCameraEngine.takeVideoSnapshot(stub, null, fileDescriptor); + } else { + throw new IllegalStateException("file and fileDescriptor are both null."); + } mUiHandler.post(new Runnable() { @Override public void run() { @@ -1852,6 +1877,33 @@ public void onCameraError(@NonNull CameraException exception) { takeVideo(file, fileDescriptor); } + /** + * Starts recording a fast, low quality video snapshot. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * Recording will be automatically stopped after the given duration, overriding + * temporarily any duration limit set by {@link #setVideoMaxDuration(int)}. + * + * @param file a file where the video will be saved + * @param durationMillis recording max duration + */ + public void takeVideoSnapshot(@NonNull File file, int durationMillis) { + takeVideoSnapshot(file, null, durationMillis); + } + + /** + * Starts recording a fast, low quality video snapshot. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * Recording will be automatically stopped after the given duration, overriding + * temporarily any duration limit set by {@link #setVideoMaxDuration(int)}. + * + * @param fileDescriptor a file descriptor where the video will be saved + * @param durationMillis recording max duration + */ + @SuppressWarnings("unused") + public void takeVideoSnapshot(@NonNull FileDescriptor fileDescriptor, int durationMillis) { + takeVideoSnapshot(null, fileDescriptor, durationMillis); + } + /** * Starts recording a fast, low quality video snapshot. Video will be written to the given file, * so callers should ensure they have appropriate permissions to write to the file. @@ -1865,7 +1917,8 @@ public void onCameraError(@NonNull CameraException exception) { * @param durationMillis recording max duration * */ - public void takeVideoSnapshot(@NonNull File file, int durationMillis) { + public void takeVideoSnapshot(@Nullable File file, @Nullable FileDescriptor fileDescriptor, + int durationMillis) { final int old = getVideoMaxDuration(); addCameraListener(new CameraListener() { @Override @@ -1884,7 +1937,7 @@ public void onCameraError(@NonNull CameraException exception) { } }); setVideoMaxDuration(durationMillis); - takeVideoSnapshot(file); + takeVideoSnapshot(file,fileDescriptor); } // TODO: pauseVideo and resumeVideo? There is mediarecorder.pause(), but API 24... diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java index b96a8aec..e978986f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java @@ -352,15 +352,15 @@ public final void setFacing(final @NonNull Facing facing) { mFacing = facing; getOrchestrator().scheduleStateful("facing", CameraState.ENGINE, new Runnable() { - @Override - public void run() { - if (collectCameraInfo(facing)) { - restart(); - } else { - mFacing = old; - } - } - }); + @Override + public void run() { + if (collectCameraInfo(facing)) { + restart(); + } else { + mFacing = old; + } + } + }); } } @@ -401,11 +401,11 @@ public final void setMode(@NonNull Mode mode) { mMode = mode; getOrchestrator().scheduleStateful("mode", CameraState.ENGINE, new Runnable() { - @Override - public void run() { - restart(); - } - }); + @Override + public void run() { + restart(); + } + }); } } @@ -508,20 +508,20 @@ public final boolean isTakingPicture() { final boolean metering = mPictureMetering; getOrchestrator().scheduleStateful("take picture", CameraState.BIND, new Runnable() { - @Override - public void run() { - LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture()); - if (isTakingPicture()) return; - if (mMode == Mode.VIDEO) { - throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); - } - stub.isSnapshot = false; - stub.location = mLocation; - stub.facing = mFacing; - stub.format = mPictureFormat; - onTakePicture(stub, metering); - } - }); + @Override + public void run() { + LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture()); + if (isTakingPicture()) return; + if (mMode == Mode.VIDEO) { + throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); + } + stub.isSnapshot = false; + stub.location = mLocation; + stub.facing = mFacing; + stub.format = mPictureFormat; + onTakePicture(stub, metering); + } + }); } /** @@ -535,20 +535,20 @@ public void run() { final boolean metering = mPictureSnapshotMetering; getOrchestrator().scheduleStateful("take picture snapshot", CameraState.BIND, new Runnable() { - @Override - public void run() { - LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture()); - if (isTakingPicture()) return; - stub.location = mLocation; - stub.isSnapshot = true; - stub.facing = mFacing; - stub.format = PictureFormat.JPEG; - // Leave the other parameters to subclasses. - //noinspection ConstantConditions - AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); - onTakePictureSnapshot(stub, ratio, metering); - } - }); + @Override + public void run() { + LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture()); + if (isTakingPicture()) return; + stub.location = mLocation; + stub.isSnapshot = true; + stub.facing = mFacing; + stub.format = PictureFormat.JPEG; + // Leave the other parameters to subclasses. + //noinspection ConstantConditions + AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); + onTakePictureSnapshot(stub, ratio, metering); + } + }); } @Override @@ -612,29 +612,36 @@ public void run() { * @param file the output file */ @Override - public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub, - @NonNull final File file) { + public final void takeVideoSnapshot(final @NonNull VideoResult.Stub stub, + final @Nullable File file, + final @Nullable FileDescriptor fileDescriptor) { getOrchestrator().scheduleStateful("take video snapshot", CameraState.BIND, new Runnable() { - @Override - public void run() { - LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo()); - stub.file = file; - stub.isSnapshot = true; - stub.videoCodec = mVideoCodec; - stub.audioCodec = mAudioCodec; - stub.location = mLocation; - stub.facing = mFacing; - stub.videoBitRate = mVideoBitRate; - stub.audioBitRate = mAudioBitRate; - stub.audio = mAudio; - stub.maxSize = mVideoMaxSize; - stub.maxDuration = mVideoMaxDuration; - //noinspection ConstantConditions - AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); - onTakeVideoSnapshot(stub, ratio); - } - }); + @Override + public void run() { + LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo()); + if (file != null) { + stub.file = file; + } else if (fileDescriptor != null) { + stub.fileDescriptor = fileDescriptor; + } else { + throw new IllegalStateException("file and fileDescriptor are both null."); + } + stub.isSnapshot = true; + stub.videoCodec = mVideoCodec; + stub.audioCodec = mAudioCodec; + stub.location = mLocation; + stub.facing = mFacing; + stub.videoBitRate = mVideoBitRate; + stub.audioBitRate = mAudioBitRate; + stub.audio = mAudio; + stub.maxSize = mVideoMaxSize; + stub.maxDuration = mVideoMaxDuration; + //noinspection ConstantConditions + AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); + onTakeVideoSnapshot(stub, ratio); + } + }); } @Override @@ -706,21 +713,21 @@ public final void onSurfaceChanged() { LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW)); getOrchestrator().scheduleStateful("surface changed", CameraState.BIND, new Runnable() { - @Override - public void run() { - // Compute a new camera preview size and apply. - Size newSize = computePreviewStreamSize(); - if (newSize.equals(mPreviewStreamSize)) { - LOG.i("onSurfaceChanged:", - "The computed preview size is identical. No op."); - } else { - LOG.i("onSurfaceChanged:", - "Computed a new preview size. Calling onPreviewStreamSizeChanged()."); - mPreviewStreamSize = newSize; - onPreviewStreamSizeChanged(); - } - } - }); + @Override + public void run() { + // Compute a new camera preview size and apply. + Size newSize = computePreviewStreamSize(); + if (newSize.equals(mPreviewStreamSize)) { + LOG.i("onSurfaceChanged:", + "The computed preview size is identical. No op."); + } else { + LOG.i("onSurfaceChanged:", + "Computed a new preview size. Calling onPreviewStreamSizeChanged()."); + mPreviewStreamSize = newSize; + onPreviewStreamSizeChanged(); + } + } + }); } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index 0614789d..d14da62d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -382,15 +382,15 @@ private Task startEngine() { return mOrchestrator.scheduleStateChange(CameraState.OFF, CameraState.ENGINE, true, new Callable>() { - @Override - public Task call() { - if (!collectCameraInfo(getFacing())) { - LOG.e("onStartEngine:", "No camera available for facing", getFacing()); - throw new CameraException(CameraException.REASON_NO_CAMERA); - } - return onStartEngine(); - } - }).onSuccessTask(new SuccessContinuation() { + @Override + public Task call() { + if (!collectCameraInfo(getFacing())) { + LOG.e("onStartEngine:", "No camera available for facing", getFacing()); + throw new CameraException(CameraException.REASON_NO_CAMERA); + } + return onStartEngine(); + } + }).onSuccessTask(new SuccessContinuation() { @NonNull @Override public Task then(@Nullable CameraOptions cameraOptions) { @@ -409,11 +409,11 @@ private Task stopEngine(boolean swallowExceptions) { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.OFF, !swallowExceptions, new Callable>() { - @Override - public Task call() { - return onStopEngine(); - } - }).addOnSuccessListener(new OnSuccessListener() { + @Override + public Task call() { + return onStopEngine(); + } + }).addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(Void aVoid) { // Put this on the outer task so we're sure it's called after getState() is OFF. @@ -464,15 +464,15 @@ private Task startBind() { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.BIND, true, new Callable>() { - @Override - public Task call() { - if (getPreview() != null && getPreview().hasSurface()) { - return onStartBind(); - } else { - return Tasks.forCanceled(); - } - } - }); + @Override + public Task call() { + if (getPreview() != null && getPreview().hasSurface()) { + return onStartBind(); + } else { + return Tasks.forCanceled(); + } + } + }); } @SuppressWarnings("UnusedReturnValue") @@ -482,11 +482,11 @@ private Task stopBind(boolean swallowExceptions) { return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.ENGINE, !swallowExceptions, new Callable>() { - @Override - public Task call() { - return onStopBind(); - } - }); + @Override + public Task call() { + return onStopBind(); + } + }); } /** @@ -517,11 +517,11 @@ private Task startPreview() { return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.PREVIEW, true, new Callable>() { - @Override - public Task call() { - return onStartPreview(); - } - }); + @Override + public Task call() { + return onStartPreview(); + } + }); } @SuppressWarnings("UnusedReturnValue") @@ -531,11 +531,11 @@ private Task stopPreview(boolean swallowExceptions) { return mOrchestrator.scheduleStateChange(CameraState.PREVIEW, CameraState.BIND, !swallowExceptions, new Callable>() { - @Override - public Task call() { - return onStopPreview(); - } - }); + @Override + public Task call() { + return onStopPreview(); + } + }); } /** @@ -721,7 +721,10 @@ public abstract void startAutoFocus(@Nullable Gesture gesture, public abstract void takeVideo(@NonNull VideoResult.Stub stub, @Nullable File file, @Nullable FileDescriptor fileDescriptor); - public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file); + // FileDescriptor options for Android 10+ + public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, + @Nullable File file, + @Nullable FileDescriptor fileDescriptor); public abstract void stopVideo(); //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 93f6cb02..90ea73ea 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -258,12 +258,21 @@ public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, int rotation // Engine synchronized (mEncoderEngineLock) { - mEncoderEngine = new MediaEncoderEngine(mResult.file, - videoEncoder, - audioEncoder, - mResult.maxDuration, - mResult.maxSize, - SnapshotVideoRecorder.this); + if (mResult.file != null) { + mEncoderEngine = new MediaEncoderEngine(mResult.file, + videoEncoder, + audioEncoder, + mResult.maxDuration, + mResult.maxSize, + SnapshotVideoRecorder.this); + }else{ + mEncoderEngine = new MediaEncoderEngine(mResult.fileDescriptor, + videoEncoder, + audioEncoder, + mResult.maxDuration, + mResult.maxSize, + SnapshotVideoRecorder.this); + } mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter); mEncoderEngine.start(); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java index 09d093e5..34d99bdf 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java @@ -13,6 +13,7 @@ import androidx.annotation.RequiresApi; import java.io.File; +import java.io.FileDescriptor; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; @@ -111,16 +112,64 @@ public interface Listener { private int mPossibleEndReason; /** - * Creates a new engine for the given file, with the given encoders and max limits, + * Creates a new engine for the given fileDescriptor, with the given encoders and max limits, * and listener to receive events. * - * @param file output file + * @param fileDescriptor output fileDescriptor * @param videoEncoder video encoder to use * @param audioEncoder audio encoder to use * @param maxDuration max duration in millis * @param maxSize max size * @param listener a listener */ + public MediaEncoderEngine(@NonNull FileDescriptor fileDescriptor, + @NonNull VideoMediaEncoder videoEncoder, + @Nullable AudioMediaEncoder audioEncoder, + final int maxDuration, + final long maxSize, + @Nullable Listener listener) { + mListener = listener; + mEncoders.add(videoEncoder); + if (audioEncoder != null) { + mEncoders.add(audioEncoder); + } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mMediaMuxer = new MediaMuxer(fileDescriptor, + MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Trying to convert the size constraints to duration constraints, + // because they are super easy to check. + // This is really naive & probably not accurate, but... + int bitRate = 0; + for (MediaEncoder encoder : mEncoders) { + bitRate += encoder.getEncodedBitRate(); + } + int byteRate = bitRate / 8; + long sizeMaxDurationUs = (maxSize / byteRate) * 1000L * 1000L; + long maxDurationUs = maxDuration * 1000L; + long finalMaxDurationUs = Long.MAX_VALUE; + if (maxSize > 0 && maxDuration > 0) { + mPossibleEndReason = sizeMaxDurationUs < maxDurationUs ? END_BY_MAX_SIZE + : END_BY_MAX_DURATION; + finalMaxDurationUs = Math.min(sizeMaxDurationUs, maxDurationUs); + } else if (maxSize > 0) { + mPossibleEndReason = END_BY_MAX_SIZE; + finalMaxDurationUs = sizeMaxDurationUs; + } else if (maxDuration > 0) { + mPossibleEndReason = END_BY_MAX_DURATION; + finalMaxDurationUs = maxDurationUs; + } + LOG.w("Computed a max duration of", (finalMaxDurationUs / 1000000F)); + for (MediaEncoder encoder : mEncoders) { + encoder.prepare(mController, finalMaxDurationUs); + } + } + public MediaEncoderEngine(@NonNull File file, @NonNull VideoMediaEncoder videoEncoder, @Nullable AudioMediaEncoder audioEncoder, @@ -349,7 +398,7 @@ public void write(@NonNull OutputBufferPool pool, @NonNull OutputBuffer buffer) Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(buffer.info.presentationTimeUs / 1000); LOG.v("write:", "Writing into muxer -", - "track:", buffer.trackIndex, + "track:", buffer.trackIndex, "presentation:", buffer.info.presentationTimeUs, "readable:", calendar.get(Calendar.SECOND) + ":" + calendar.get(Calendar.MILLISECOND), diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 23969cce..eac126c6 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -14,6 +14,7 @@ android { vectorDrawables.useSupportLibrary = true } sourceSets["main"].java.srcDir("src/main/kotlin") + namespace = "com.otaliastudios.cameraview.demo" } dependencies { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 5b2f05ca..6c18d635 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt index 5c5311ce..88d0807b 100644 --- a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt @@ -1,11 +1,15 @@ package com.otaliastudios.cameraview.demo import android.animation.ValueAnimator +import android.content.ContentResolver +import android.content.ContentValues import android.content.Intent -import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED import android.graphics.* +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.MediaStore import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -22,6 +26,7 @@ import com.otaliastudios.cameraview.frame.Frame import com.otaliastudios.cameraview.frame.FrameProcessor import java.io.ByteArrayOutputStream import java.io.File +import java.text.SimpleDateFormat import java.util.* class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Callback { @@ -38,7 +43,9 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal private var currentFilter = 0 private val allFilters = Filters.values() - + private lateinit var resolver: ContentResolver + private var isSnapVideo = false + private var uri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) @@ -55,20 +62,29 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay) if (DECODE_BITMAP) { if (frame.format == ImageFormat.NV21 - && frame.dataClass == ByteArray::class.java) { + && frame.dataClass == ByteArray::class.java + ) { val data = frame.getData() - val yuvImage = YuvImage(data, - frame.format, - frame.size.width, - frame.size.height, - null) + val yuvImage = YuvImage( + data, + frame.format, + frame.size.width, + frame.size.height, + null + ) val jpegStream = ByteArrayOutputStream() - yuvImage.compressToJpeg(Rect(0, 0, + yuvImage.compressToJpeg( + Rect( + 0, 0, frame.size.width, - frame.size.height), 100, jpegStream) + frame.size.height + ), 100, jpegStream + ) val jpegByteArray = jpegStream.toByteArray() - val bitmap = BitmapFactory.decodeByteArray(jpegByteArray, - 0, jpegByteArray.size) + val bitmap = BitmapFactory.decodeByteArray( + jpegByteArray, + 0, jpegByteArray.size + ) bitmap.toString() } } @@ -85,45 +101,45 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal val group = controlPanel.getChildAt(0) as ViewGroup val watermark = findViewById(R.id.watermark) val options: List> = listOf( - // Layout - Option.Width(), Option.Height(), - // Engine and preview - Option.Mode(), Option.Engine(), Option.Preview(), - // Some controls - Option.Flash(), Option.WhiteBalance(), Option.Hdr(), - Option.PictureMetering(), Option.PictureSnapshotMetering(), - Option.PictureFormat(), - // Video recording - Option.PreviewFrameRate(), Option.VideoCodec(), Option.Audio(), Option.AudioCodec(), - // Gestures - Option.Pinch(), Option.HorizontalScroll(), Option.VerticalScroll(), - Option.Tap(), Option.LongTap(), - // Watermarks - Option.OverlayInPreview(watermark), - Option.OverlayInPictureSnapshot(watermark), - Option.OverlayInVideoSnapshot(watermark), - // Frame Processing - Option.FrameProcessingFormat(), - // Other - Option.Grid(), Option.GridColor(), Option.UseDeviceOrientation() + // Layout + Option.Width(), Option.Height(), + // Engine and preview + Option.Mode(), Option.Engine(), Option.Preview(), + // Some controls + Option.Flash(), Option.WhiteBalance(), Option.Hdr(), + Option.PictureMetering(), Option.PictureSnapshotMetering(), + Option.PictureFormat(), + // Video recording + Option.PreviewFrameRate(), Option.VideoCodec(), Option.Audio(), Option.AudioCodec(), + // Gestures + Option.Pinch(), Option.HorizontalScroll(), Option.VerticalScroll(), + Option.Tap(), Option.LongTap(), + // Watermarks + Option.OverlayInPreview(watermark), + Option.OverlayInPictureSnapshot(watermark), + Option.OverlayInVideoSnapshot(watermark), + // Frame Processing + Option.FrameProcessingFormat(), + // Other + Option.Grid(), Option.GridColor(), Option.UseDeviceOrientation() ) val dividers = listOf( - // Layout - false, true, - // Engine and preview - false, false, true, - // Some controls - false, false, false, false, false, true, - // Video recording - false, false, false, true, - // Gestures - false, false, false, false, true, - // Watermarks - false, false, true, - // Frame Processing - true, - // Other - false, false, true + // Layout + false, true, + // Engine and preview + false, false, true, + // Some controls + false, false, false, false, false, true, + // Video recording + false, false, false, true, + // Gestures + false, false, false, false, true, + // Watermarks + false, false, true, + // Frame Processing + true, + // Other + false, false, true ) for (i in options.indices) { val view = OptionView(this) @@ -194,6 +210,12 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal override fun onVideoTaken(result: VideoResult) { super.onVideoTaken(result) + if (isSnapVideo && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val fileDetails = + ContentValues().apply { put(MediaStore.Video.Media.IS_PENDING, 0) } + uri?.let { resolver.update(it, fileDetails, null, null) } + VideoPreviewActivity.fdUri = uri; + } LOG.w("onVideoTaken called! Launching activity.") VideoPreviewActivity.videoResult = result val intent = Intent(this@CameraActivity, VideoPreviewActivity::class.java) @@ -212,7 +234,11 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal LOG.w("onVideoRecordingEnd!") } - override fun onExposureCorrectionChanged(newValue: Float, bounds: FloatArray, fingers: Array?) { + override fun onExposureCorrectionChanged( + newValue: Float, + bounds: FloatArray, + fingers: Array? + ) { super.onExposureCorrectionChanged(newValue, bounds, fingers) message("Exposure correction:$newValue", false) } @@ -269,6 +295,7 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal } private fun captureVideo() { + isSnapVideo = false; if (camera.mode == Mode.PICTURE) return run { message("Can't record HQ videos while in PICTURE mode.", false) } @@ -285,7 +312,48 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal message("Video snapshots are only allowed with the GL_SURFACE preview.", true) } message("Recording snapshot for 5 seconds...", true) - camera.takeVideoSnapshot(File(filesDir, "video.mp4"), 5000) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + //this is uri + isSnapVideo = true; + val videoCollection = + MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + var fileName: String = getFileName("mp4"); + val videoDetails = ContentValues().apply { + put( + MediaStore.Video.Media.DISPLAY_NAME, + fileName + )//todo: create random name (use time) + put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") + put(MediaStore.Video.Media.IS_PENDING, 1) + put(MediaStore.Video.Media.ALBUM, "CameraView") + } + //resolver set already + resolver = contentResolver + uri = resolver.insert(videoCollection, videoDetails) + val videoFileDescriptor = + uri?.let { resolver.openFileDescriptor(it, "w", null)?.fileDescriptor } + + if (videoFileDescriptor != null) { + camera.takeVideoSnapshot(videoFileDescriptor, 5000) + } + + } else { + isSnapVideo = false; + camera.takeVideoSnapshot(File(filesDir, "video.mp4"), 5000) + } + + } + + private fun getFileName(type: String): String { + var ext = ".mp4" + var pre = "VID-" + val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timeStamp = sdf.format(Date()) + if (type.equals("img")) { + ext = ".jpg" + pre = "IMG-" + } + return pre + timeStamp + ext } private fun toggleCamera() { @@ -323,8 +391,10 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal val preview = camera.preview val wrapContent = value as Int == WRAP_CONTENT if (preview == Preview.SURFACE && !wrapContent) { - message("The SurfaceView preview does not support width or height changes. " + - "The view will act as WRAP_CONTENT by default.", true) + message( + "The SurfaceView preview does not support width or height changes. " + + "The view will act as WRAP_CONTENT by default.", true + ) return false } } @@ -334,7 +404,11 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal return true } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) val valid = grantResults.all { it == PERMISSION_GRANTED } if (valid && !camera.isOpened) { diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt index c105d4dd..f58ad859 100644 --- a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt @@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.demo import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.Log import android.view.Menu @@ -17,6 +18,7 @@ import com.otaliastudios.cameraview.size.AspectRatio class VideoPreviewActivity : AppCompatActivity() { companion object { var videoResult: VideoResult? = null + var fdUri:Uri?=null } private val videoView: VideoView by lazy { findViewById(R.id.video) } @@ -54,7 +56,12 @@ class VideoPreviewActivity : AppCompatActivity() { controller.setAnchorView(videoView) controller.setMediaPlayer(videoView) videoView.setMediaController(controller) - videoView.setVideoURI(Uri.fromFile(result.file)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + + videoView.setVideoURI(fdUri) + }else{ + videoView.setVideoURI(Uri.fromFile(result.file)) + } videoView.setOnPreparedListener { mp -> val lp = videoView.layoutParams val videoWidth = mp.videoWidth.toFloat() @@ -94,8 +101,8 @@ class VideoPreviewActivity : AppCompatActivity() { val intent = Intent(Intent.ACTION_SEND) intent.type = "video/*" val uri = FileProvider.getUriForFile(this, - this.packageName + ".provider", - videoResult!!.file) + this.packageName + ".provider", + videoResult!!.file) intent.putExtra(Intent.EXTRA_STREAM, uri) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) startActivity(intent) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 14d834c6..7d58f47a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip