Am developing a simple camera app. I have code that takes screenshot of the whole activity and writes it to the Sd card. the problem is that the Surfaceview returns a black
My situation related with ExoPlayer, need get bitmap of current frame.
Work on API >= 24.
private val copyFrameHandler = Handler()
fun getFrameBitmap(callback: FrameBitmapCallback) {
when(val view = videoSurfaceView) {
is TextureView -> callback.onResult(view.bitmap)
is SurfaceView -> {
val bitmap = Bitmap.createBitmap(
videoSurfaceView.getWidth(),
videoSurfaceView.getHeight(),
Bitmap.Config.ARGB_8888
)
copyFrameHandler.removeCallbacksAndMessages(null)
PixelCopy.request(view, bitmap, { copyResult: Int ->
if (copyResult == PixelCopy.SUCCESS) {
callback.onResult(bitmap)
} else {
callback.onResult(null)
}
}, copyFrameHandler)
}
else -> callback.onResult(null)
}
}
fun onDestroy() {
copyFrameHandler.removeCallbacksAndMessages(null)
}
interface FrameBitmapCallback {
fun onResult(bitmap: Bitmap?)
}
Just copy and past code
Note: only For API level >= 24
private void takePhoto() {
// Create a bitmap the size of the scene view.
final Bitmap bitmap = Bitmap.createBitmap(surfaceView.getWidth(), surfaceView.getHeight(),
Bitmap.Config.ARGB_8888);
// Create a handler thread to offload the processing of the image.
final HandlerThread handlerThread = new HandlerThread("PixelCopier");
handlerThread.start();
// Make the request to copy.
PixelCopy.request(holder.videoView, bitmap, (copyResult) -> {
if (copyResult == PixelCopy.SUCCESS) {
Log.e(TAG,bitmap.toString());
String name = String.valueOf(System.currentTimeMillis() + ".jpg");
imageFile = ScreenshotUtils.store(bitmap,name);
} else {
Toast toast = Toast.makeText(getViewActivity(),
"Failed to copyPixels: " + copyResult, Toast.LENGTH_LONG);
toast.show();
}
handlerThread.quitSafely();
}, new Handler(handlerThread.getLooper()));
}
If your situation allows it, using a TextureView
instead of a SurfaceView
will make this problem a lot easier to solve. It has a method getBitmap() that returns a Bitmap
of the current frame on the TextureView
.
This is how I do it. Put this method in some Util class
/**
* Pixel copy to copy SurfaceView/VideoView into BitMap
*/
fun usePixelCopy(videoView: SurfaceView, callback: (Bitmap?) -> Unit) {
val bitmap: Bitmap = Bitmap.createBitmap(
videoView.width,
videoView.height,
Bitmap.Config.ARGB_8888
);
try {
// Create a handler thread to offload the processing of the image.
val handlerThread = HandlerThread("PixelCopier");
handlerThread.start();
PixelCopy.request(
videoView, bitmap,
PixelCopy.OnPixelCopyFinishedListener { copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
callback(bitmap)
}
handlerThread.quitSafely();
},
Handler(handlerThread.looper)
)
} catch (e: IllegalArgumentException) {
callback(null)
// PixelCopy may throw IllegalArgumentException, make sure to handle it
e.printStackTrace()
}
}
Usage:
usePixelCopy(videoView) { bitmap: Bitmap? ->
processBitMp(bitmap)
}
Note: Video View is a Subclass of SurfaceView so this method can take screenshot of video View as well
For those of us looking for a solution for API <= 24, this is what I did as a workaround. When user clicks button for capture, close the preview captureSession and create a new captureSession for still image capture.
mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();
mCaptureSession.close();
mCaptureSession = null;
mPreviewReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.JPEG, MAX_IMAGES);
mPreviewReader.setOnImageAvailableListener(mImageAvailableListener, mBackgroundHandler);
mCameraDevice.createCaptureSession(
Arrays.asList(mPreviewReader.getSurface()),
//Arrays.asList(mSurface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mCaptureSession = cameraCaptureSession;
try {
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mPreviewReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(mOrientation));
Log.d(TAG, "Capture request created.");
mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, mBackgroundHandler);
} catch (CameraAccessException cae) {
Log.d(TAG, cae.toString());
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
}
},
mBackgroundHandler
);
Then at onImageAvailableListener, you can get the image with imageReader.acquireNextImage()
to get the still captured image.
The SurfaceView's surface is independent of the surface on which View elements are drawn. So capturing the View contents won't include the SurfaceView.
You need to capture the SurfaceView contents separately and perform your own composition step. The easiest way to do the capture is probably to just re-render the contents, but use an off-screen bitmap as the target rather than the surface. If you're rendering with GLES to an off-screen pbuffer, you can use glReadPixels()
before you swap buffers.
Update: Grafika's "texture from camera" activity demonstrates handling live video from the camera with OpenGL ES. EglSurfaceBase#saveFrame()
shows how to capture GLES rendering to a Bitmap.
Update: See also this answer, which provides a bit more background.