From 7e16f014fd83b58eb7c01ec9d63ca785a2387344 Mon Sep 17 00:00:00 2001 From: mirmilad Date: Wed, 22 Jan 2020 11:28:54 +0330 Subject: [PATCH] Capturing camera and storing in sequential segmented files. --- README.md | 2 +- .../grafika/CameraCaptureActivity.java | 26 ++++++-- .../com/android/grafika/MainActivity.java | 5 +- .../java/com/android/grafika/MiscUtils.java | 10 ++++ .../android/grafika/TextureMovieEncoder.java | 13 +++- .../com/android/grafika/VideoEncoderCore.java | 59 ++++++++++++++++++- .../res/layout/activity_camera_capture.xml | 20 ++++++- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 119 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3a98f0ad..e30add71 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ requires API level 23 (Marshmallow) or greater. The shader used for the filters is not optimized, but seems to perform well on most devices (the original Nexus 7 (2012) being a notable exception). Demo here: http://www.youtube.com/watch?v=kH9kCP2T5Gg -- The output is a video-only MP4 file ("camera-test.mp4"). +- The output is a video-only MP4 file ("camera-test.mp4") or you can store the video in sequential segmented files ("camera-test-1.mp4", "camera-test-2.mp4", ...). For storing in multiple files, you must set `Output Segment Duration` in UI. [Simple Canvas in TextureView](app/src/main/java/com/android/grafika/TextureViewCanvasActivity.java). Exercises software rendering to a `TextureView` with a `Canvas`. - Renders as quickly as possible. Because it's using software rendering, this will likely diff --git a/app/src/main/java/com/android/grafika/CameraCaptureActivity.java b/app/src/main/java/com/android/grafika/CameraCaptureActivity.java index 522d1c85..3c462ef3 100644 --- a/app/src/main/java/com/android/grafika/CameraCaptureActivity.java +++ b/app/src/main/java/com/android/grafika/CameraCaptureActivity.java @@ -16,11 +16,15 @@ package com.android.grafika; +import android.app.Activity; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; import android.opengl.EGL14; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; @@ -34,11 +38,9 @@ import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; -import android.app.Activity; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; import android.widget.Toast; import com.android.grafika.gles.FullFrameRect; @@ -138,6 +140,7 @@ public class CameraCaptureActivity extends Activity static final int FILTER_EDGE_DETECT = 4; static final int FILTER_EMBOSS = 5; + private EditText mSegmentDurationEditText; private GLSurfaceView mGLView; private CameraSurfaceRenderer mRenderer; private Camera mCamera; @@ -154,10 +157,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera_capture); - File outputFile = new File(getFilesDir(), "camera-test.mp4"); + File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_DCIM), "camera-test.mp4"); TextView fileText = (TextView) findViewById(R.id.cameraOutputFile_text); fileText.setText(outputFile.toString()); + mSegmentDurationEditText = findViewById(R.id.segmentDuration_edit); + Spinner spinner = (Spinner) findViewById(R.id.cameraFilter_spinner); ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.cameraFilterNames, android.R.layout.simple_spinner_item); @@ -353,6 +358,7 @@ public void clickToggleRecording(@SuppressWarnings("unused") View unused) { mGLView.queueEvent(new Runnable() { @Override public void run() { // notify the renderer that we want to change the encoder's state + mRenderer.setSegmentDuration(Integer.parseInt(mSegmentDurationEditText.getText().toString())); mRenderer.changeRecordingState(mRecordingEnabled); } }); @@ -494,7 +500,7 @@ class CameraSurfaceRenderer implements GLSurfaceView.Renderer { private int mCurrentFilter; private int mNewFilter; - + private int mSegmentDuration; /** * Constructs CameraSurfaceRenderer. *

@@ -692,7 +698,7 @@ public void onDrawFrame(GL10 unused) { Log.d(TAG, "START recording"); // start recording mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig( - mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext())); + mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext(), mSegmentDuration)); mRecordingStatus = RECORDING_ON; break; case RECORDING_RESUMED: @@ -771,4 +777,12 @@ private void drawBox() { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDisable(GLES20.GL_SCISSOR_TEST); } + + public int getSegmentDuration() { + return mSegmentDuration; + } + + public void setSegmentDuration(int segmentDuration) { + this.mSegmentDuration = segmentDuration; + } } diff --git a/app/src/main/java/com/android/grafika/MainActivity.java b/app/src/main/java/com/android/grafika/MainActivity.java index 1cb423ad..7eb6afd1 100644 --- a/app/src/main/java/com/android/grafika/MainActivity.java +++ b/app/src/main/java/com/android/grafika/MainActivity.java @@ -16,10 +16,9 @@ package com.android.grafika; -import android.os.Bundle; import android.app.ListActivity; import android.content.Intent; -import android.util.Log; +import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -80,7 +79,7 @@ public class MainActivity extends ListActivity { "Exercises SurfaceFlinger PTS handling", "ScheduledSwapActivity" }, { "Show + capture camera", - "Shows camera preview, records when requested", + "Shows camera preview, records when requested. You can record in a single file or sequential segmented files", "CameraCaptureActivity" }, { "Simple GL in TextureView", "Renders with GL as quickly as possible", diff --git a/app/src/main/java/com/android/grafika/MiscUtils.java b/app/src/main/java/com/android/grafika/MiscUtils.java index 2b9b2c3e..36df7a09 100644 --- a/app/src/main/java/com/android/grafika/MiscUtils.java +++ b/app/src/main/java/com/android/grafika/MiscUtils.java @@ -100,4 +100,14 @@ public static long getDisplayRefreshNsec(Activity activity) { Log.d(TAG, "refresh rate is " + displayFps + " fps --> " + refreshNs + " ns"); return refreshNs; } + + /** + * Calculate the greatest common divisor (gcd) of two integer + */ + public static int gcd(int a, int b) + { + if (b == 0) + return a; + return gcd(b, a % b); + } } diff --git a/app/src/main/java/com/android/grafika/TextureMovieEncoder.java b/app/src/main/java/com/android/grafika/TextureMovieEncoder.java index 8e969cca..8ffe5163 100644 --- a/app/src/main/java/com/android/grafika/TextureMovieEncoder.java +++ b/app/src/main/java/com/android/grafika/TextureMovieEncoder.java @@ -101,14 +101,21 @@ public static class EncoderConfig { final int mHeight; final int mBitRate; final EGLContext mEglContext; + final int mSegmentDuration; public EncoderConfig(File outputFile, int width, int height, int bitRate, EGLContext sharedEglContext) { + this(outputFile, width, height, bitRate, sharedEglContext, 0); + } + + public EncoderConfig(File outputFile, int width, int height, int bitRate, + EGLContext sharedEglContext, int segmentDuration) { mOutputFile = outputFile; mWidth = width; mHeight = height; mBitRate = bitRate; mEglContext = sharedEglContext; + mSegmentDuration = segmentDuration; } @Override @@ -310,7 +317,7 @@ private void handleStartRecording(EncoderConfig config) { Log.d(TAG, "handleStartRecording " + config); mFrameNum = 0; prepareEncoder(config.mEglContext, config.mWidth, config.mHeight, config.mBitRate, - config.mOutputFile); + config.mOutputFile, config.mSegmentDuration); } /** @@ -376,9 +383,9 @@ private void handleUpdateSharedContext(EGLContext newSharedContext) { } private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate, - File outputFile) { + File outputFile, int segmentDuration) { try { - mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile); + mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile, segmentDuration); } catch (IOException ioe) { throw new RuntimeException(ioe); } diff --git a/app/src/main/java/com/android/grafika/VideoEncoderCore.java b/app/src/main/java/com/android/grafika/VideoEncoderCore.java index dbc3dfea..5541f7c1 100644 --- a/app/src/main/java/com/android/grafika/VideoEncoderCore.java +++ b/app/src/main/java/com/android/grafika/VideoEncoderCore.java @@ -52,13 +52,24 @@ public class VideoEncoderCore { private MediaCodec.BufferInfo mBufferInfo; private int mTrackIndex; private boolean mMuxerStarted; - + private final File mOutputFile; + private int mSegmentIndex = 1; + private final long mSegmentDurationUsec; + private long mLastPresentationTimeUs = -1; /** * Configures encoder and muxer state, and prepares the input Surface. */ public VideoEncoderCore(int width, int height, int bitRate, File outputFile) throws IOException { + this(width, height, bitRate, outputFile, 0); + } + + /** + * Configures encoder and muxer state, and prepares the input Surface. + */ + public VideoEncoderCore(int width, int height, int bitRate, File outputFile, int segmentDurationSec) + throws IOException { mBufferInfo = new MediaCodec.BufferInfo(); MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); @@ -69,7 +80,7 @@ public VideoEncoderCore(int width, int height, int bitRate, File outputFile) MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, MiscUtils.gcd(IFRAME_INTERVAL, segmentDurationSec)); if (VERBOSE) Log.d(TAG, "format: " + format); // Create a MediaCodec encoder, and configure it with our format. Get a Surface @@ -85,7 +96,9 @@ public VideoEncoderCore(int width, int height, int bitRate, File outputFile) // // We're not actually interested in multiplexing audio. We just want to convert // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file. - mMuxer = new MediaMuxer(outputFile.toString(), + mSegmentDurationUsec = segmentDurationSec * 1000000; + mOutputFile = outputFile; + mMuxer = new MediaMuxer(getNextOutputFileName(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); mTrackIndex = -1; @@ -99,6 +112,23 @@ public Surface getInputSurface() { return mInputSurface; } + /** + * Returns sequential new filename if segmentDuration be grater than 0 + */ + private String getNextOutputFileName() { + if(mSegmentDurationUsec > 0) { + String ext = ""; + String fileName = mOutputFile.getName(); + int extIndex = fileName.lastIndexOf('.'); + if(extIndex > 0) { + ext = fileName.substring(extIndex); + fileName = fileName.substring(0, extIndex); + } + return new File(mOutputFile.getParent(), fileName + "-" + mSegmentIndex++ + ext).toString(); + } else + return mOutputFile.toString(); + } + /** * Releases encoder resources. */ @@ -189,6 +219,29 @@ public void drainEncoder(boolean endOfStream) { encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); + if(mLastPresentationTimeUs == -1) + mLastPresentationTimeUs = mBufferInfo.presentationTimeUs; + + if(mSegmentDurationUsec > 0 && (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0 && + mBufferInfo.presentationTimeUs - mLastPresentationTimeUs >= mSegmentDurationUsec) { + mLastPresentationTimeUs = mBufferInfo.presentationTimeUs; + + mMuxer.stop(); + mMuxer.release(); + mMuxer = null; + + try { + String newMuxerPath = getNextOutputFileName(); + if (VERBOSE) Log.d(TAG, "creating new muxer: " + newMuxerPath); + mMuxer = new MediaMuxer(newMuxerPath, + MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + } catch (IOException e) { + throw new RuntimeException("Can not create new muxer", e); + } + mTrackIndex = mMuxer.addTrack(mEncoder.getOutputFormat()); + mMuxer.start(); + } + mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); if (VERBOSE) { Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" + diff --git a/app/src/main/res/layout/activity_camera_capture.xml b/app/src/main/res/layout/activity_camera_capture.xml index 3f552e96..ac3082cb 100644 --- a/app/src/main/res/layout/activity_camera_capture.xml +++ b/app/src/main/res/layout/activity_camera_capture.xml @@ -31,7 +31,7 @@ android:layout_alignLeft="@+id/toggleRecording_button" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" - android:layout_below="@+id/toggleRecording_button" + android:layout_below="@+id/segmentDuration_edit" android:layout_centerInParent="true" > + + + + diff --git a/build.gradle b/build.gradle index 3eb903f0..9051c0a6 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.3' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 51e83a01..8a69f0b8 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-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip