Providing video recording functionality with ARCore - android
I'm working with this sample (https://github.com/google-ar/arcore-android-sdk/tree/master/samples/hello_ar_java), and I want to provide the functionality to record a video with the AR objects placed.
I tried multiple things but to no avail, is there a recommended way to do it?
Creating a video from an OpenGL surface is a little involved, but is doable. The easiest way to understand I think is to use two EGL surfaces, one for the UI and one for the media encoder. There is a good example of the EGL level calls needed in the Grafika project on GitHub. I used that as starting point to figure out the modifications needed to the HelloAR sample for ARCore. Since there are quite a few changes, I broke it down into steps.
Make changes to support writing to external storage
To save the video, you need to write the video file somewhere accessible, so you need to get this permission.
Declare the permission in the AndroidManifest.xml file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Then change CameraPermissionHelper.java to request the external storage permission as well as the camera permission. To do this, make an array of the permissions and use that when requesting the permissions and iterate over it when checking the permission state:
private static final String REQUIRED_PERMISSIONS[] = {
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
public static void requestCameraPermission(Activity activity) {
ActivityCompat.requestPermissions(activity, REQUIRED_PERMISSIONS,
CAMERA_PERMISSION_CODE);
}
public static boolean hasCameraPermission(Activity activity) {
for(String p : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(activity, p) !=
PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
public static boolean shouldShowRequestPermissionRationale(Activity activity) {
for(String p : REQUIRED_PERMISSIONS) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, p)) {
return true;
}
}
return false;
}
Add recording to HelloARActivity
Add a simple button and text view to the UI at the bottom of activity_main.xml:
<Button
android:id="#+id/fboRecord_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="#+id/surfaceview"
android:layout_alignTop="#+id/surfaceview"
android:onClick="clickToggleRecording"
android:text="#string/toggleRecordingOn"
tools:ignore="OnClick"/>
<TextView
android:id="#+id/nowRecording_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="#+id/fboRecord_button"
android:layout_alignBottom="#+id/fboRecord_button"
android:layout_toEndOf="#+id/fboRecord_button"
android:text="" />
In HelloARActivity add member variables for recording:
private VideoRecorder mRecorder;
private android.opengl.EGLConfig mAndroidEGLConfig;
Initialize mAndroidEGLConfig in onSurfaceCreated(). We'll use this config object to create the encoder surface.
EGL10 egl10 = (EGL10)EGLContext.getEGL();
javax.microedition.khronos.egl.EGLDisplay display = egl10.eglGetCurrentDisplay();
int v[] = new int[2];
egl10.eglGetConfigAttrib(display,config, EGL10.EGL_CONFIG_ID, v);
EGLDisplay androidDisplay = EGL14.eglGetCurrentDisplay();
int attribs[] = {EGL14.EGL_CONFIG_ID, v[0], EGL14.EGL_NONE};
android.opengl.EGLConfig myConfig[] = new android.opengl.EGLConfig[1];
EGL14.eglChooseConfig(androidDisplay, attribs, 0, myConfig, 0, 1, v, 1);
this.mAndroidEGLConfig = myConfig[0];
Refactor the onDrawFrame() method so all the non-drawing code is executed first, and the actual drawing is done in a method called draw(). This way during recording, we can update the ARCore frame, process the input, then draw to the UI, and draw again to the encoder.
#Override
public void onDrawFrame(GL10 gl) {
if (mSession == null) {
return;
}
// Notify ARCore session that the view size changed so that
// the perspective matrix and
// the video background can be properly adjusted.
mDisplayRotationHelper.updateSessionIfNeeded(mSession);
try {
// Obtain the current frame from ARSession. When the
//configuration is set to
// UpdateMode.BLOCKING (it is by default), this will
// throttle the rendering to the camera framerate.
Frame frame = mSession.update();
Camera camera = frame.getCamera();
// Handle taps. Handling only one tap per frame, as taps are
// usually low frequency compared to frame rate.
MotionEvent tap = mQueuedSingleTaps.poll();
if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) {
for (HitResult hit : frame.hitTest(tap)) {
// Check if any plane was hit, and if it was hit inside the plane polygon
Trackable trackable = hit.getTrackable();
if (trackable instanceof Plane
&& ((Plane) trackable).isPoseInPolygon(hit.getHitPose())) {
// Cap the number of objects created. This avoids overloading both the
// rendering system and ARCore.
if (mAnchors.size() >= 20) {
mAnchors.get(0).detach();
mAnchors.remove(0);
}
// Adding an Anchor tells ARCore that it should track this position in
// space. This anchor is created on the Plane to place the 3d model
// in the correct position relative both to the world and to the plane.
mAnchors.add(hit.createAnchor());
// Hits are sorted by depth. Consider only closest hit on a plane.
break;
}
}
}
// Get projection matrix.
float[] projmtx = new float[16];
camera.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);
// Get camera matrix and draw.
float[] viewmtx = new float[16];
camera.getViewMatrix(viewmtx, 0);
// Compute lighting from average intensity of the image.
final float lightIntensity = frame.getLightEstimate().getPixelIntensity();
// Visualize tracked points.
PointCloud pointCloud = frame.acquirePointCloud();
mPointCloud.update(pointCloud);
draw(frame,camera.getTrackingState() == TrackingState.PAUSED,
viewmtx, projmtx, camera.getDisplayOrientedPose(),lightIntensity);
if (mRecorder!= null && mRecorder.isRecording()) {
VideoRecorder.CaptureContext ctx = mRecorder.startCapture();
if (ctx != null) {
// draw again
draw(frame, camera.getTrackingState() == TrackingState.PAUSED,
viewmtx, projmtx, camera.getDisplayOrientedPose(), lightIntensity);
// restore the context
mRecorder.stopCapture(ctx, frame.getTimestamp());
}
}
// Application is responsible for releasing the point cloud resources after
// using it.
pointCloud.release();
// Check if we detected at least one plane. If so, hide the loading message.
if (mMessageSnackbar != null) {
for (Plane plane : mSession.getAllTrackables(Plane.class)) {
if (plane.getType() ==
com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING
&& plane.getTrackingState() == TrackingState.TRACKING) {
hideLoadingMessage();
break;
}
}
}
} catch (Throwable t) {
// Avoid crashing the application due to unhandled exceptions.
Log.e(TAG, "Exception on the OpenGL thread", t);
}
}
private void draw(Frame frame, boolean paused,
float[] viewMatrix, float[] projectionMatrix,
Pose displayOrientedPose, float lightIntensity) {
// Clear screen to notify driver it should not load
// any pixels from previous frame.
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// Draw background.
mBackgroundRenderer.draw(frame);
// If not tracking, don't draw 3d objects.
if (paused) {
return;
}
mPointCloud.draw(viewMatrix, projectionMatrix);
// Visualize planes.
mPlaneRenderer.drawPlanes(
mSession.getAllTrackables(Plane.class),
displayOrientedPose, projectionMatrix);
// Visualize anchors created by touch.
float scaleFactor = 1.0f;
for (Anchor anchor : mAnchors) {
if (anchor.getTrackingState() != TrackingState.TRACKING) {
continue;
}
// Get the current pose of an Anchor in world space.
// The Anchor pose is
// updated during calls to session.update() as ARCore refines
// its estimate of the world.
anchor.getPose().toMatrix(mAnchorMatrix, 0);
// Update and draw the model and its shadow.
mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
mVirtualObject.draw(viewMatrix, projectionMatrix, lightIntensity);
mVirtualObjectShadow.draw(viewMatrix, projectionMatrix, lightIntensity);
}
}
Handle the toggling of recording:
public void clickToggleRecording(View view) {
Log.d(TAG, "clickToggleRecording");
if (mRecorder == null) {
File outputFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES) + "/HelloAR",
"fbo-gl-" + Long.toHexString(System.currentTimeMillis()) + ".mp4");
File dir = outputFile.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try {
mRecorder = new VideoRecorder(mSurfaceView.getWidth(),
mSurfaceView.getHeight(),
VideoRecorder.DEFAULT_BITRATE, outputFile, this);
mRecorder.setEglConfig(mAndroidEGLConfig);
} catch (IOException e) {
Log.e(TAG,"Exception starting recording", e);
}
}
mRecorder.toggleRecording();
updateControls();
}
private void updateControls() {
Button toggleRelease = findViewById(R.id.fboRecord_button);
int id = (mRecorder != null && mRecorder.isRecording()) ?
R.string.toggleRecordingOff : R.string.toggleRecordingOn;
toggleRelease.setText(id);
TextView tv = findViewById(R.id.nowRecording_text);
if (id == R.string.toggleRecordingOff) {
tv.setText(getString(R.string.nowRecording));
} else {
tv.setText("");
}
}
Add a listener interface to receive video recording state changes:
#Override
public void onVideoRecorderEvent(VideoRecorder.VideoEvent videoEvent) {
Log.d(TAG, "VideoEvent: " + videoEvent);
updateControls();
if (videoEvent == VideoRecorder.VideoEvent.RecordingStopped) {
mRecorder = null;
}
}
Implement the VideoRecorder class to feed images to the encoder
The VideoRecorder class is used to feed the images to the media encoder. This class creates an off screen EGLSurface using the input surface of the media encoder. The general approach is during recording draw once for the UI display, and then make the same exact draw call for the media encoder surface.
The constructor takes recording parameters and a listener to push events to during the recording process.
public VideoRecorder(int width, int height, int bitrate, File outputFile,
VideoRecorderListener listener) throws IOException {
this.listener = listener;
mEncoderCore = new VideoEncoderCore(width, height, bitrate, outputFile);
mVideoRect = new Rect(0,0,width,height);
}
When recording starts, we need to create a new EGL surface for the encoder. Then notify the encoder that a new frame is available, make the encoder surface the current EGL surface, and return so the caller can make the drawing calls.
public CaptureContext startCapture() {
if (mVideoEncoder == null) {
return null;
}
if (mEncoderContext == null) {
mEncoderContext = new CaptureContext();
mEncoderContext.windowDisplay = EGL14.eglGetCurrentDisplay();
// Create a window surface, and attach it to the Surface we received.
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
mEncoderContext.windowDrawSurface = EGL14.eglCreateWindowSurface(
mEncoderContext.windowDisplay,
mEGLConfig,mEncoderCore.getInputSurface(),
surfaceAttribs, 0);
mEncoderContext.windowReadSurface = mEncoderContext.windowDrawSurface;
}
CaptureContext displayContext = new CaptureContext();
displayContext.initialize();
// Draw for recording, swap.
mVideoEncoder.frameAvailableSoon();
// Make the input surface current
// mInputWindowSurface.makeCurrent();
EGL14.eglMakeCurrent(mEncoderContext.windowDisplay,
mEncoderContext.windowDrawSurface, mEncoderContext.windowReadSurface,
EGL14.eglGetCurrentContext());
// If we don't set the scissor rect, the glClear() we use to draw the
// light-grey background will draw outside the viewport and muck up our
// letterboxing. Might be better if we disabled the test immediately after
// the glClear(). Of course, if we were clearing the frame background to
// black it wouldn't matter.
//
// We do still need to clear the pixels outside the scissor rect, of course,
// or we'll get garbage at the edges of the recording. We can either clear
// the whole thing and accept that there will be a lot of overdraw, or we
// can issue multiple scissor/clear calls. Some GPUs may have a special
// optimization for zeroing out the color buffer.
//
// For now, be lazy and zero the whole thing. At some point we need to
// examine the performance here.
GLES20.glClearColor(0f, 0f, 0f, 1f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glViewport(mVideoRect.left, mVideoRect.top,
mVideoRect.width(), mVideoRect.height());
GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
GLES20.glScissor(mVideoRect.left, mVideoRect.top,
mVideoRect.width(), mVideoRect.height());
return displayContext;
}
When the drawing is completed, the EGLContext needs to be restored back to the UI surface:
public void stopCapture(CaptureContext oldContext, long timeStampNanos) {
if (oldContext == null) {
return;
}
GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
EGLExt.eglPresentationTimeANDROID(mEncoderContext.windowDisplay,
mEncoderContext.windowDrawSurface, timeStampNanos);
EGL14.eglSwapBuffers(mEncoderContext.windowDisplay,
mEncoderContext.windowDrawSurface);
// Restore.
GLES20.glViewport(0, 0, oldContext.getWidth(), oldContext.getHeight());
EGL14.eglMakeCurrent(oldContext.windowDisplay,
oldContext.windowDrawSurface, oldContext.windowReadSurface,
EGL14.eglGetCurrentContext());
}
Add some bookkeeping methods
public boolean isRecording() {
return mRecording;
}
public void toggleRecording() {
if (isRecording()) {
stopRecording();
} else {
startRecording();
}
}
protected void startRecording() {
mRecording = true;
if (mVideoEncoder == null) {
mVideoEncoder = new TextureMovieEncoder2(mEncoderCore);
}
if (listener != null) {
listener.onVideoRecorderEvent(VideoEvent.RecordingStarted);
}
}
protected void stopRecording() {
mRecording = false;
if (mVideoEncoder != null) {
mVideoEncoder.stopRecording();
}
if (listener != null) {
listener.onVideoRecorderEvent(VideoEvent.RecordingStopped);
}
}
public void setEglConfig(EGLConfig eglConfig) {
this.mEGLConfig = eglConfig;
}
public enum VideoEvent {
RecordingStarted,
RecordingStopped
}
public interface VideoRecorderListener {
void onVideoRecorderEvent(VideoEvent videoEvent);
}
The inner class for the CaptureContext keeps track of the display and surfaces in order to easily handle multiple surfaces being used with the EGL context:
public static class CaptureContext {
EGLDisplay windowDisplay;
EGLSurface windowReadSurface;
EGLSurface windowDrawSurface;
private int mWidth;
private int mHeight;
public void initialize() {
windowDisplay = EGL14.eglGetCurrentDisplay();
windowReadSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
windowDrawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_READ);
int v[] = new int[1];
EGL14.eglQuerySurface(windowDisplay, windowDrawSurface, EGL14.EGL_WIDTH,
v, 0);
mWidth = v[0];
v[0] = -1;
EGL14.eglQuerySurface(windowDisplay, windowDrawSurface, EGL14.EGL_HEIGHT,
v, 0);
mHeight = v[0];
}
/**
* Returns the surface's width, in pixels.
* <p>
* If this is called on a window surface, and the underlying
* surface is in the process
* of changing size, we may not see the new size right away
* (e.g. in the "surfaceChanged"
* callback). The size should match after the next buffer swap.
*/
public int getWidth() {
if (mWidth < 0) {
int v[] = new int[1];
EGL14.eglQuerySurface(windowDisplay,
windowDrawSurface, EGL14.EGL_WIDTH, v, 0);
mWidth = v[0];
}
return mWidth;
}
/**
* Returns the surface's height, in pixels.
*/
public int getHeight() {
if (mHeight < 0) {
int v[] = new int[1];
EGL14.eglQuerySurface(windowDisplay, windowDrawSurface,
EGL14.EGL_HEIGHT, v, 0);
mHeight = v[0];
}
return mHeight;
}
}
Add VideoEncoder classes
The VideoEncoderCore class is copied from Grafika, as well as the TextureMovieEncoder2 class.
Related
How to remove plane surface in arcore after the model is loaded in anchor?
I m trying to remove plane surface after object is loaded in Arcore. I don't know how to remove the plane surface. My existing code is written below.I would appreciate if some one can help with that. there must be some function to call from PlaneRender.java but i dont see that could remove the plane surface after the model is loaded. #Override public void onDrawFrame(GL10 gl) { /* if (isObjReplaced) { isObjReplaced = false; try { virtualObject.createOnGlThread(this, objName, textureName); virtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f); } catch (IOException e) { e.printStackTrace(); } return; }*/ // Clear screen to notify driver it should not load any pixels from previous frame. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); if (session == null) { return; } // Notify ARCore session that the view size changed so that the perspective matrix and // the video background can be properly adjusted. displayRotationHelper.updateSessionIfNeeded(session); try { session.setCameraTextureName(backgroundRenderer.getTextureId()); // Obtain the current frame from ARSession. When the configuration is set to // UpdateMode.BLOCKING (it is by default), this will throttle the rendering to the // camera framerate. Frame frame = session.update(); Camera camera = frame.getCamera(); // Handle taps. Handling only one tap per frame, as taps are usually low frequency // compared to frame rate. MotionEvent tap = queuedSingleTaps.poll(); if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) { for (HitResult hit : frame.hitTest(tap)) { // Check if any plane was hit, and if it was hit inside the plane polygon Trackable trackable = hit.getTrackable(); // Creates an anchor if a plane or an oriented point was hit. if ((trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hit.getHitPose())) || (trackable instanceof Point && ((Point) trackable).getOrientationMode() == OrientationMode.ESTIMATED_SURFACE_NORMAL)) { // Hits are sorted by depth. Consider only closest hit on a plane or oriented point. // Cap the number of objects created. This avoids overloading both the // rendering system and ARCore. if (anchors.size() >= 1) { anchors.get(0).detach(); anchors.remove(0); } // Adding an Anchor tells ARCore that it should track this position in // space. This anchor is created on the Plane to place the 3D model // in the correct position relative both to the world and to the plane. anchors.add(hit.createAnchor()); break; } } } // Draw background. backgroundRenderer.draw(frame); // If not tracking, don't draw 3d objects. if (camera.getTrackingState() == TrackingState.PAUSED) { return; } // Get projection matrix. float[] projmtx = new float[16]; camera.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f); // Get camera matrix and draw. float[] viewmtx = new float[16]; camera.getViewMatrix(viewmtx, 0); // Compute lighting from average intensity of the image. final float lightIntensity = frame.getLightEstimate().getPixelIntensity(); // Visualize tracked points. PointCloud pointCloud = frame.acquirePointCloud(); this.pointCloud.update(pointCloud); this.pointCloud.draw(viewmtx, projmtx); // Application is responsible for releasing the point cloud resources after // using it. pointCloud.release(); // Check if we detected at least one plane. If so, hide the loading message. if (messageSnackbar != null) { for (Plane plane : session.getAllTrackables(Plane.class)) { if (plane.getType() == Plane.Type.HORIZONTAL_UPWARD_FACING && plane.getTrackingState() == TrackingState.TRACKING) { hideLoadingMessage(); break; } } } // Visualize planes. planeRenderer.drawPlanes( session.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projmtx); // Visualize anchors created by touch. float scaleFactor = 1.0f; for (Anchor anchor : anchors) { if (anchor.getTrackingState() != TrackingState.TRACKING) { continue; } else{ anchor.getPose().toMatrix(anchorMatrix, 0); // Update and draw the model and its shadow. virtualObject.updateModelMatrix(anchorMatrix, GlobalClass.scaleFactor); virtualObject.draw(viewmtx, projmtx, lightIntensity); } // Get the current pose of an Anchor in world space. The Anchor pose is updated // during calls to session.update() as ARCore refines its estimate of the world. } } catch (Throwable t) { // Avoid crashing the application due to unhandled exceptions. Log.e(TAG, "Exception on the OpenGL thread", t); } }
Dynamic focus area for android camera
I am building an android camera app (not using camera2 api) to take close-range pictures of some objects in the outdoor conditions. The pictures needs to be taken in burst mode i.e. once activated the camera will take say 5 pics continuously and all the pics needs to be well focussed. The user may be moving the camera while taking pics and user will not be able to manually choose the point of focus. The objects are dark in color and sometimes the camera is being over-exposed by bright objects in the camera view. I know how to set focus area as camera parameters but the position of focus area has to be changed automatically so that its always focussed on the dark regions in the camera view. The position of dark objects is not fixed in camera view and so the app will have to look for dark pixels in every frame before setting focus area. I am thinking of checking for dark regions in the onPreviewFrame() callback but I am not sure if this is the correct way to do that. Has anyone done this before who can point me in the right direction? For example is there a project which will make android camera focus on a face always using a face detector? I tried to look on internet but could not find any relevant projects.
You have to implement touched focus. Domething like this: #Override public boolean onTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ float x = event.getX(); float y = event.getY(); float touchMajor = event.getTouchMajor(); float touchMinor = event.getTouchMinor(); Rect touchRect = new Rect( (int)(x - touchMajor/2), (int)(y - touchMinor/2), (int)(x + touchMajor/2), (int)(y + touchMinor/2)); if (mTouchEventListener != null) mTouchEventListener.touchFocus(touchRect, false); } where touchFocus looks like this: #TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void touchFocus(Rect tfocusRect, boolean useInMid) { if (mCamera == null) return; try{ mCamera.cancelAutoFocus(); //Convert from View's width and height to +/- 1000 Rect targetFocusRect = (useInMid || sfv == null) ? new Rect() : new Rect(tfocusRect.left * 2000/sfv.getWidth() - 1000, tfocusRect.top * 2000/sfv.getHeight() - 1000, tfocusRect.right * 2000/sfv.getWidth() - 1000, tfocusRect.bottom * 2000/sfv.getHeight() - 1000); final List<Camera.Area> focusList = new ArrayList<Camera.Area>(); Camera.Area focusArea = new Camera.Area(targetFocusRect, 1000); focusList.add(focusArea); Parameters para = mCamera.getParameters(); O.Log.d(TAG,para.getMaxNumFocusAreas() + ";" + para.getMaxNumMeteringAreas() + " >> " + tfocusRect.toString()); para.setFocusAreas(focusList); para.setMeteringAreas(focusList); try{ mCamera.setParameters(para); }catch(RuntimeException e){ O.Log.e(TAG, "setParameters failed", e); } mCamera.autoFocus(myAutoFocusCallback); // _.setCameraTorch(1); }catch (Exception e){ O.Log.e(TAG, "Touch Focus Camera Error", e); } } private static AutoFocusCallback myAutoFocusCallback = new AutoFocusCallback() { #Override public void onAutoFocus(boolean success, Camera camera) { // TODO Auto-generated method stub } }; for new API read this post http://www.morethantechnical.com/2017/02/28/android-camera2-touch-to-focus/ //Override in your touch-enabled view (this can be differen than the view you use for displaying the cam preview) #Override public boolean onTouch(View view, MotionEvent motionEvent) { final int actionMasked = motionEvent.getActionMasked(); if (actionMasked != MotionEvent.ACTION_DOWN) { return false; } if (mManualFocusEngaged) { Log.d(TAG, "Manual focus already engaged"); return true; } final Rect sensorArraySize = mCameraInfo.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); //TODO: here I just flip x,y, but this needs to correspond with the sensor orientation (via SENSOR_ORIENTATION) final int y = (int)((motionEvent.getX() / (float)view.getWidth()) * (float)sensorArraySize.height()); final int x = (int)((motionEvent.getY() / (float)view.getHeight()) * (float)sensorArraySize.width()); final int halfTouchWidth = 150; //(int)motionEvent.getTouchMajor(); //TODO: this doesn't represent actual touch size in pixel. Values range in [3, 10]... final int halfTouchHeight = 150; //(int)motionEvent.getTouchMinor(); MeteringRectangle focusAreaTouch = new MeteringRectangle(Math.max(x - halfTouchWidth, 0), Math.max(y - halfTouchHeight, 0), halfTouchWidth * 2, halfTouchHeight * 2, MeteringRectangle.METERING_WEIGHT_MAX - 1); CameraCaptureSession.CaptureCallback captureCallbackHandler = new CameraCaptureSession.CaptureCallback() { #Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); mManualFocusEngaged = false; if (request.getTag() == "FOCUS_TAG") { //the focus trigger is complete - //resume repeating (preview surface will get frames), clear AF trigger mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, null); mCameraOps.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); } } #Override public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { super.onCaptureFailed(session, request, failure); Log.e(TAG, "Manual AF failure: " + failure); mManualFocusEngaged = false; } }; //first stop the existing repeating request mCameraOps.stopRepeating(); //cancel any existing AF trigger (repeated touches, etc.) mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); mCameraOps.capture(mPreviewRequestBuilder.build(), captureCallbackHandler, mBackgroundHandler); //Now add a new AF trigger with focus region if (isMeteringAreaAFSupported()) { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusAreaTouch}); } mPreviewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); mPreviewRequestBuilder.setTag("FOCUS_TAG"); //we'll capture this later for resuming the preview //then we ask for a single request (not repeating!) mCameraOps.capture(mPreviewRequestBuilder.build(), captureCallbackHandler, mBackgroundHandler); mManualFocusEngaged = true; return true; } private boolean isMeteringAreaAFSupported() { return mCameraInfo.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) >= 1; }
Draw text or image on the camera stream (GLSL)
I have a live broadcasting app based off grafika's examples, where I send my video feed over RTMP to be live broadcast. I now want to watermark my video by overlaying text or a logo on my video stream. I know this can be done with GLSL filtering, but I have no idea how to implement this based on the sample that I linked. I tried using Alpha blending but it seems the two texture formats are somehow incompatible (one being TEXTURE_EXTERNAL_OES and the other one TEXTURE_2D) and I just get a black frame in return. EDIT: I based my code on Kickflip API: class CameraSurfaceRenderer implements GLSurfaceView.Renderer { private static final String TAG = "CameraSurfaceRenderer"; private static final boolean VERBOSE = false; private CameraEncoder mCameraEncoder; private FullFrameRect mFullScreenCamera; private FullFrameRect mFullScreenOverlay; // For texture overlay private final float[] mSTMatrix = new float[16]; private int mOverlayTextureId; private int mCameraTextureId; private boolean mRecordingEnabled; private int mFrameCount; // Keep track of selected filters + relevant state private boolean mIncomingSizeUpdated; private int mIncomingWidth; private int mIncomingHeight; private int mCurrentFilter; private int mNewFilter; boolean showBox = false; /** * Constructs CameraSurfaceRenderer. * <p> * #param recorder video encoder object */ public CameraSurfaceRenderer(CameraEncoder recorder) { mCameraEncoder = recorder; mCameraTextureId = -1; mFrameCount = -1; SessionConfig config = recorder.getConfig(); mIncomingWidth = config.getVideoWidth(); mIncomingHeight = config.getVideoHeight(); mIncomingSizeUpdated = true; // Force texture size update on next onDrawFrame mCurrentFilter = -1; mNewFilter = Filters.FILTER_NONE; mRecordingEnabled = false; } /** * Notifies the renderer that we want to stop or start recording. */ public void changeRecordingState(boolean isRecording) { Log.d(TAG, "changeRecordingState: was " + mRecordingEnabled + " now " + isRecording); mRecordingEnabled = isRecording; } #Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { Log.d(TAG, "onSurfaceCreated"); // Set up the texture blitter that will be used for on-screen display. This // is *not* applied to the recording, because that uses a separate shader. mFullScreenCamera = new FullFrameRect( new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); // For texture overlay: GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); mFullScreenOverlay = new FullFrameRect( new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_2D)); mOverlayTextureId = GlUtil.createTextureWithTextContent("hello!"); mOverlayTextureId = GlUtil.createTextureFromImage(mCameraView.getContext(), R.drawable.red_dot); mCameraTextureId = mFullScreenCamera.createTextureObject(); mCameraEncoder.onSurfaceCreated(mCameraTextureId); mFrameCount = 0; } #Override public void onSurfaceChanged(GL10 unused, int width, int height) { Log.d(TAG, "onSurfaceChanged " + width + "x" + height); } #Override public void onDrawFrame(GL10 unused) { if (VERBOSE){ if(mFrameCount % 30 == 0){ Log.d(TAG, "onDrawFrame tex=" + mCameraTextureId); mCameraEncoder.logSavedEglState(); } } if (mCurrentFilter != mNewFilter) { Filters.updateFilter(mFullScreenCamera, mNewFilter); mCurrentFilter = mNewFilter; mIncomingSizeUpdated = true; } if (mIncomingSizeUpdated) { mFullScreenCamera.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); mFullScreenOverlay.getProgram().setTexSize(mIncomingWidth, mIncomingHeight); mIncomingSizeUpdated = false; Log.i(TAG, "setTexSize on display Texture"); } // Draw the video frame. if(mCameraEncoder.isSurfaceTextureReadyForDisplay()){ mCameraEncoder.getSurfaceTextureForDisplay().updateTexImage(); mCameraEncoder.getSurfaceTextureForDisplay().getTransformMatrix(mSTMatrix); //Drawing texture overlay: mFullScreenOverlay.drawFrame(mOverlayTextureId, mSTMatrix); mFullScreenCamera.drawFrame(mCameraTextureId, mSTMatrix); } mFrameCount++; } public void signalVertialVideo(FullFrameRect.SCREEN_ROTATION isVertical) { if (mFullScreenCamera != null) mFullScreenCamera.adjustForVerticalVideo(isVertical, false); } /** * Changes the filter that we're applying to the camera preview. */ public void changeFilterMode(int filter) { mNewFilter = filter; } public void handleTouchEvent(MotionEvent ev){ mFullScreenCamera.handleTouchEvent(ev); } } This is the code for Rendering the image on the screen (GLSurfaceView), but this is not actually overlayed over the video. If I am not mistaken, this is done on CameraEncoder. Thing is, replicating the code from CameraSurfaceRenderer into CameraEncoder (they both have similar code when it comes to filters) does not provide an overlayed text/image.
The texture object uses the GL_TEXTURE_EXTERNAL_OES texture target, which is defined by the GL_OES_EGL_image_external OpenGL ES extension. This limits how the texture may be used. Each time the texture is bound it must be bound to the GL_TEXTURE_EXTERNAL_OES target rather than the GL_TEXTURE_2D target. Additionally, any OpenGL ES 2.0 shader that samples from the texture must declare its use of this extension using, for example, an "#extension GL_OES_EGL_image_external : require" directive. Such shaders must also access the texture using the samplerExternalOES GLSL sampler type. https://developer.android.com/reference/android/graphics/SurfaceTexture.html Post your code that you used to do alpha blending and I can probably fix it. I would probably override the Texture2dProgram and pass that to the FullFrame Renderer. It has example code for rendering using the GL_TEXTURE_EXTERNAL_OES extension. Basically, #Override the draw function, call the base implementation, bind your watermark and draw. That should be between camera and the video encoder.
Surface Texture object is not getting the frames from a Surface Class
On the one hand, I have a Surface Class which when instantiated, automatically initialize a new thread and start grabbing frames from a streaming source via native code based on FFMPEG. Here is the main parts of the code for the aforementioned Surface Class: public class StreamingSurface extends Surface implements Runnable { ... public StreamingSurface(SurfaceTexture surfaceTexture, int width, int height) { super(surfaceTexture); screenWidth = width; screenHeight = height; init(); } public void init() { mDrawTop = 0; mDrawLeft = 0; mVideoCurrentFrame = 0; this.setVideoFile(); this.startPlay(); } public void setVideoFile() { // Initialise FFMPEG naInit(""); // Get stream video res int[] res = naGetVideoRes(); mDisplayWidth = (int)(res[0]); mDisplayHeight = (int)(res[1]); // Prepare Display mBitmap = Bitmap.createBitmap(mDisplayWidth, mDisplayHeight, Bitmap.Config.ARGB_8888); naPrepareDisplay(mBitmap, mDisplayWidth, mDisplayHeight); } public void startPlay() { thread = new Thread(this); thread.start(); } #Override public void run() { while (true) { while (2 == mStatus) { //pause SystemClock.sleep(100); } mVideoCurrentFrame = naGetVideoFrame(); if (0 < mVideoCurrentFrame) { //success, redraw if(isValid()){ Canvas canvas = lockCanvas(null); if (null != mBitmap) { canvas.drawBitmap(mBitmap, mDrawLeft, mDrawTop, prFramePaint); } unlockCanvasAndPost(canvas); } } else { //failure, probably end of video, break naFinish(mBitmap); mStatus = 0; break; } } } } In my MainActivity class, I instantiated this class in the following way: public void startCamera(int texture) { mSurface = new SurfaceTexture(texture); mSurface.setOnFrameAvailableListener(this); Surface surface = new StreamingSurface(mSurface, 640, 360); surface.release(); } I read the following line in the Android developer page, regarding the Surface class constructor: "Images drawn to the Surface will be made available to the SurfaceTexture, which can attach them to an OpenGL ES texture via updateTexImage()." That is exactly what I want to do, and I have everything ready for the further renderization. But definitely, with the above code, I never get my frames captured in the surface class transformed to its corresponding SurfaceTexture. I know this because the debugger, for instace, never call the OnFrameAvailableLister method associated with that Surface Texture. Any ideas? Maybe the fact that I am using a thread to call the drawing functions is messing everything? In such a case, what alternatives I have to grab the frames? Thanks in advance
Playing Video with OpenGL and MediaCodec
I'm trying to play the same video at the same time in two different textureviews. I've used code from grafika (MoviePlayer and ContinuousCaptureActivity) to try to get it to work (thanks fadden). To make the problem simpler, I'm trying to do it with just one TextureView first. At the moment I've created a TextureView, and once it get a SurfaceTexture, I create a WindowSurface and make it current. Then I generate a TextureID generated using a FullFrameRect object. #Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mSurfaceTexture = surface; mEGLCore = new EglCore(null, EglCore.FLAG_TRY_GLES3); Log.d("EglCore", "EGL core made"); mDisplaySurface = new WindowSurface(mEGLCore, mSurfaceTexture); mDisplaySurface.makeCurrent(); Log.d("DisplaySurface", "mDisplaySurface made"); mFullFrameBlit = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); mTextureID = mFullFrameBlit.createTextureObject(); //mSurfaceTexture.attachToGLContext(mTextureID); clickPlayStop(null); } Then I get an off-screen SurfaceTexture, link it with the TextureID that I got above and create a surface to pass to a MoviePlayer thus: public void clickPlayStop(#SuppressWarnings("unused") View unused) { if (mShowStopLabel) { Log.d(TAG, "stopping movie"); stopPlayback(); // Don't update the controls here -- let the task thread do it after the movie has // actually stopped. //mShowStopLabel = false; //updateControls(); } else { if (mPlayTask != null) { Log.w(TAG, "movie already playing"); return; } Log.d(TAG, "starting movie"); SpeedControlCallback callback = new SpeedControlCallback(); callback.setFixedPlaybackRate(24); MoviePlayer player = null; MovieTexture = new SurfaceTexture(mTextureID); MovieTexture.setOnFrameAvailableListener(this); Surface surface = new Surface(MovieTexture); try { player = new MoviePlayer(surface, callback, this);//TODO } catch (IOException ioe) { Log.e(TAG, "Unable to play movie", ioe); return; } adjustAspectRatio(player.getVideoWidth(), player.getVideoHeight()); mPlayTask = new MoviePlayer.PlayTask(player, this); mPlayTask.setLoopMode(true); mShowStopLabel = true; mPlayTask.execute(); } } The idea is that the SurfaceTexture gets a raw frame which I can use as an OES_external texture to sample from with OpenGL. Then I can call DrawFrame() from my EGLContext after setting my WindowSurface as current. private void drawFrame() { Log.d(TAG, "drawFrame"); if (mEGLCore == null) { Log.d(TAG, "Skipping drawFrame after shutdown"); return; } // Latch the next frame from the camera. mDisplaySurface.makeCurrent(); MovieTexture.updateTexImage(); MovieTexture.getTransformMatrix(mTransformMatrix); // Fill the WindowSurface with it. int viewWidth = mTextureView.getWidth(); int viewHeight = mTextureView.getHeight(); GLES20.glViewport(0, 0, viewWidth, viewHeight); mFullFrameBlit.drawFrame(mTextureID, mTransformMatrix); mDisplaySurface.swapBuffers(); } If I wanted to do it with 2 TextureViews, the idea would be to call makeCurrent() and draw into each buffer for each view, then call swapBuffers() after the drawing is done. This is what I want to do, but I am pretty sure this is not what my code is actually doing. Could somebody help me understand what I need to change to make it work? #Fadden Update: This is interesting. I changed the code in onSurfaceTextureAvailable to this: #Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mSurfaceTexture = surface; TextureHeight = height; TextureWidth = width; //mEGLCore = new EglCore(null, EglCore.FLAG_TRY_GLES3); Log.d("EglCore", "EGL core made"); //mDisplaySurface = new WindowSurface(mEGLCore, mSurfaceTexture); //mDisplaySurface.makeCurrent(); Log.d("DisplaySurface", "mDisplaySurface made"); //mFullFrameBlit = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.OPENGL_TEST)); //mTextureID = mFullFrameBlit.createTextureObject(); //clickPlayStop(null); // Fill the SurfaceView with it. //int viewWidth = width; //int viewHeight = height; //GLES20.glViewport(0, 0, viewWidth, viewHeight); //mFullFrameBlit.drawFrame(mTextureID, mTransformMatrix); //mFullFrameBlit.openGLTest(); //mFullFrameBlit.testDraw(mDisplaySurface.getHeight(),mDisplaySurface.getWidth()); //mDisplaySurface.swapBuffers(); } So, it shouldn't call anything else, just show the empty TextureView - and this is what I see...
Thanks to Fadden for the help. So there seemed to be some unknown issue that was resolved when I used a new thread to decode and produce the frames. I haven't found out what caused the original problem, but I have found a way around it.