OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.media; |
| 6 |
| 7 import android.annotation.TargetApi; |
| 8 import android.app.Activity; |
| 9 import android.app.Fragment; |
| 10 import android.app.FragmentManager; |
| 11 import android.app.FragmentTransaction; |
| 12 import android.content.Context; |
| 13 import android.content.Intent; |
| 14 import android.graphics.ImageFormat; |
| 15 import android.graphics.PixelFormat; |
| 16 import android.hardware.display.DisplayManager; |
| 17 import android.hardware.display.VirtualDisplay; |
| 18 import android.media.Image; |
| 19 import android.media.ImageReader; |
| 20 import android.media.projection.MediaProjection; |
| 21 import android.media.projection.MediaProjectionManager; |
| 22 import android.os.Build; |
| 23 import android.os.Handler; |
| 24 import android.os.HandlerThread; |
| 25 import android.util.DisplayMetrics; |
| 26 import android.view.Display; |
| 27 import android.view.Surface; |
| 28 |
| 29 import org.chromium.base.ApplicationStatus; |
| 30 import org.chromium.base.Log; |
| 31 import org.chromium.base.annotations.CalledByNative; |
| 32 import org.chromium.base.annotations.JNINamespace; |
| 33 |
| 34 import java.nio.ByteBuffer; |
| 35 |
| 36 /** |
| 37 * This class implements Screen Capture using projection API, introduced in Andr
oid |
| 38 * API 21 (L Release). Capture takes place in the current Looper, while pixel |
| 39 * download takes place in another thread used by ImageReader. |
| 40 **/ |
| 41 @JNINamespace("media") |
| 42 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 43 public class ScreenCapture extends Fragment { |
| 44 private static final String TAG = "ScreenCaptureMachine"; |
| 45 |
| 46 private static final int REQUEST_MEDIA_PROJECTION = 1; |
| 47 |
| 48 // Native callback context variable. |
| 49 private final long mNativeScreenCaptureMachineAndroid; |
| 50 private final Context mContext; |
| 51 |
| 52 private static enum CaptureState { ATTACHED, ALLOWED, STARTED, STOPPING, STO
PPED } |
| 53 private final Object mCaptureStateLock = new Object(); |
| 54 private CaptureState mCaptureState = CaptureState.STOPPED; |
| 55 |
| 56 private MediaProjection mMediaProjection; |
| 57 private MediaProjectionManager mMediaProjectionManager; |
| 58 private VirtualDisplay mVirtualDisplay; |
| 59 private Surface mSurface; |
| 60 private ImageReader mImageReader = null; |
| 61 private HandlerThread mThread; |
| 62 private Handler mBackgroundHandler; |
| 63 |
| 64 private int mScreenDensity; |
| 65 private int mWidth; |
| 66 private int mHeight; |
| 67 private int mFormat; |
| 68 private int mResultCode; |
| 69 private Intent mResultData; |
| 70 |
| 71 ScreenCapture(Context context, long nativeScreenCaptureMachineAndroid) { |
| 72 mContext = context; |
| 73 mNativeScreenCaptureMachineAndroid = nativeScreenCaptureMachineAndroid; |
| 74 |
| 75 Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| 76 if (activity == null) { |
| 77 Log.e(TAG, "activity is null"); |
| 78 return; |
| 79 } |
| 80 |
| 81 FragmentManager fragmentManager = activity.getFragmentManager(); |
| 82 FragmentTransaction fragmentTransaction = fragmentManager.beginTransacti
on(); |
| 83 fragmentTransaction.add(this, "screencapture"); |
| 84 |
| 85 try { |
| 86 fragmentTransaction.commit(); |
| 87 } catch (RuntimeException e) { |
| 88 Log.e(TAG, "ScreenCaptureExcaption " + e); |
| 89 } |
| 90 |
| 91 DisplayMetrics metrics = new DisplayMetrics(); |
| 92 Display display = activity.getWindowManager().getDefaultDisplay(); |
| 93 display.getMetrics(metrics); |
| 94 mScreenDensity = metrics.densityDpi; |
| 95 } |
| 96 |
| 97 // Factory method. |
| 98 @CalledByNative |
| 99 static ScreenCapture createScreenCaptureMachine( |
| 100 Context context, long nativeScreenCaptureMachineAndroid) { |
| 101 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| 102 return new ScreenCapture(context, nativeScreenCaptureMachineAndroid)
; |
| 103 } |
| 104 return null; |
| 105 } |
| 106 |
| 107 // Internal class implementing the ImageReader listener. Gets pinged when a |
| 108 // new frame is been captured and downloaded to memory-backed buffers. |
| 109 // TODO(braveyao): This is very similar as the one in VideoCaptureCamera2. T
ry to see |
| 110 // if we can extend from it, https://crbug.com/487935. |
| 111 private class CrImageReaderListener implements ImageReader.OnImageAvailableL
istener { |
| 112 @Override |
| 113 public void onImageAvailable(ImageReader reader) { |
| 114 synchronized (mCaptureStateLock) { |
| 115 if (mCaptureState != CaptureState.STARTED) { |
| 116 Log.e(TAG, "Get captured frame in unexpected state."); |
| 117 return; |
| 118 } |
| 119 } |
| 120 |
| 121 try (Image image = reader.acquireLatestImage()) { |
| 122 if (image == null) return; |
| 123 if (reader.getWidth() != image.getWidth() |
| 124 || reader.getHeight() != image.getHeight()) { |
| 125 Log.e(TAG, "ImageReader size (" + reader.getWidth() + "x" +
reader.getHeight() |
| 126 + ") did not match Image size (" + image.get
Width() + "x" |
| 127 + image.getHeight() + ")"); |
| 128 throw new IllegalStateException(); |
| 129 } |
| 130 |
| 131 switch (image.getFormat()) { |
| 132 case PixelFormat.RGBA_8888: |
| 133 if (image.getPlanes().length != 1) { |
| 134 Log.e(TAG, "Unexpected image planes for RGBA_8888 fo
rmat: " |
| 135 + image.getPlanes().length); |
| 136 throw new IllegalStateException(); |
| 137 } |
| 138 |
| 139 nativeOnRGBAFrameAvailable(mNativeScreenCaptureMachineAn
droid, |
| 140 image.getPlanes()[0].getBuffer(), |
| 141 image.getPlanes()[0].getRowStride(), image.getCr
opRect().left, |
| 142 image.getCropRect().top, image.getCropRect().wid
th(), |
| 143 image.getCropRect().height(), image.getTimestamp
()); |
| 144 break; |
| 145 case ImageFormat.YUV_420_888: |
| 146 if (image.getPlanes().length != 3) { |
| 147 Log.e(TAG, "Unexpected image planes for YUV_420_888
format: " |
| 148 + image.getPlanes().length); |
| 149 throw new IllegalStateException(); |
| 150 } |
| 151 |
| 152 // The pixel stride of Y plane is always 1. The U/V plan
es are guaranteed |
| 153 // to have the same row stride and pixel stride. |
| 154 nativeOnI420FrameAvailable(mNativeScreenCaptureMachineAn
droid, |
| 155 image.getPlanes()[0].getBuffer(), |
| 156 image.getPlanes()[0].getRowStride(), |
| 157 image.getPlanes()[1].getBuffer(), image.getPlane
s()[2].getBuffer(), |
| 158 image.getPlanes()[1].getRowStride(), |
| 159 image.getPlanes()[1].getPixelStride(), image.get
CropRect().left, |
| 160 image.getCropRect().top, image.getCropRect().wid
th(), |
| 161 image.getCropRect().height(), image.getTimestamp
()); |
| 162 break; |
| 163 default: |
| 164 Log.e(TAG, "Unexpected image format: " + image.getFormat
()); |
| 165 throw new IllegalStateException(); |
| 166 } |
| 167 } catch (IllegalStateException ex) { |
| 168 Log.e(TAG, "acquireLatestImage():" + ex); |
| 169 } catch (UnsupportedOperationException ex) { |
| 170 Log.i(TAG, "acquireLatestImage():" + ex); |
| 171 // YUV_420_888 is the preference, but not all devices support it
, |
| 172 // fall-back to RGBA_8888 then. |
| 173 mImageReader.close(); |
| 174 mVirtualDisplay.release(); |
| 175 |
| 176 mFormat = PixelFormat.RGBA_8888; |
| 177 createImageReaderWithFormat(mFormat); |
| 178 createVirtualDisplay(); |
| 179 } |
| 180 } |
| 181 } |
| 182 |
| 183 private class MediaProjectionCallback extends MediaProjection.Callback { |
| 184 @Override |
| 185 public void onStop() { |
| 186 changeCaptureStateAndNotify(CaptureState.STOPPED); |
| 187 mMediaProjection = null; |
| 188 if (mVirtualDisplay == null) return; |
| 189 mVirtualDisplay.release(); |
| 190 mVirtualDisplay = null; |
| 191 } |
| 192 } |
| 193 |
| 194 @Override |
| 195 public void onAttach(Context context) { |
| 196 super.onAttach(context); |
| 197 Log.d(TAG, "onAttach"); |
| 198 changeCaptureStateAndNotify(CaptureState.ATTACHED); |
| 199 } |
| 200 |
| 201 // This method was deprecated in API level 23 by onAttach(Context). |
| 202 // TODO(braveyao): remove this method after the minSdkVersion of chrome is 2
3, |
| 203 // https://crbug.com/614172. |
| 204 @SuppressWarnings("deprecation") |
| 205 @Override |
| 206 public void onAttach(Activity activity) { |
| 207 super.onAttach(activity); |
| 208 Log.d(TAG, "onAttach"); |
| 209 changeCaptureStateAndNotify(CaptureState.ATTACHED); |
| 210 } |
| 211 |
| 212 @Override |
| 213 public void onDetach() { |
| 214 super.onDetach(); |
| 215 Log.d(TAG, "onDetach"); |
| 216 stopCapture(); |
| 217 } |
| 218 |
| 219 @CalledByNative |
| 220 public boolean startPrompt(int width, int height) { |
| 221 Log.d(TAG, "startPrompt"); |
| 222 synchronized (mCaptureStateLock) { |
| 223 while (mCaptureState != CaptureState.ATTACHED) { |
| 224 try { |
| 225 mCaptureStateLock.wait(); |
| 226 } catch (InterruptedException ex) { |
| 227 Log.e(TAG, "ScreenCaptureException: " + ex); |
| 228 } |
| 229 } |
| 230 } |
| 231 |
| 232 mWidth = width; |
| 233 mHeight = height; |
| 234 |
| 235 mMediaProjectionManager = (MediaProjectionManager) mContext.getSystemSer
vice( |
| 236 Context.MEDIA_PROJECTION_SERVICE); |
| 237 if (mMediaProjectionManager == null) { |
| 238 Log.e(TAG, "mMediaProjectionManager is null"); |
| 239 return false; |
| 240 } |
| 241 |
| 242 try { |
| 243 startActivityForResult( |
| 244 mMediaProjectionManager.createScreenCaptureIntent(), REQUEST
_MEDIA_PROJECTION); |
| 245 } catch (android.content.ActivityNotFoundException e) { |
| 246 Log.e(TAG, "ScreenCaptureException " + e); |
| 247 return false; |
| 248 } |
| 249 return true; |
| 250 } |
| 251 |
| 252 @Override |
| 253 public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| 254 if (requestCode != REQUEST_MEDIA_PROJECTION) return; |
| 255 |
| 256 if (resultCode == Activity.RESULT_OK) { |
| 257 mResultCode = resultCode; |
| 258 mResultData = data; |
| 259 changeCaptureStateAndNotify(CaptureState.ALLOWED); |
| 260 } |
| 261 nativeOnActivityResult( |
| 262 mNativeScreenCaptureMachineAndroid, resultCode == Activity.RESUL
T_OK); |
| 263 } |
| 264 |
| 265 @CalledByNative |
| 266 public void startCapture() { |
| 267 Log.d(TAG, "startCapture"); |
| 268 synchronized (mCaptureStateLock) { |
| 269 if (mCaptureState != CaptureState.ALLOWED) { |
| 270 Log.e(TAG, "startCapture() invoked without user permission."); |
| 271 return; |
| 272 } |
| 273 } |
| 274 mMediaProjection = mMediaProjectionManager.getMediaProjection(mResultCod
e, mResultData); |
| 275 if (mMediaProjection == null) { |
| 276 Log.e(TAG, "mMediaProjection is null"); |
| 277 return; |
| 278 } |
| 279 mMediaProjection.registerCallback(new MediaProjectionCallback(), null); |
| 280 |
| 281 mThread = new HandlerThread("ScreenCapture"); |
| 282 mThread.start(); |
| 283 mBackgroundHandler = new Handler(mThread.getLooper()); |
| 284 |
| 285 // On Android M and above, YUV420 is prefered. Some Android L devices wi
ll silently |
| 286 // fail with YUV420, so keep with RGBA_8888 on L. |
| 287 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
| 288 mFormat = PixelFormat.RGBA_8888; |
| 289 } else { |
| 290 mFormat = ImageFormat.YUV_420_888; |
| 291 } |
| 292 createImageReaderWithFormat(mFormat); |
| 293 createVirtualDisplay(); |
| 294 |
| 295 changeCaptureStateAndNotify(CaptureState.STARTED); |
| 296 } |
| 297 |
| 298 @CalledByNative |
| 299 public void stopCapture() { |
| 300 Log.d(TAG, "stopCapture"); |
| 301 synchronized (mCaptureStateLock) { |
| 302 if (mMediaProjection != null && mCaptureState == CaptureState.STARTE
D) { |
| 303 mMediaProjection.stop(); |
| 304 changeCaptureStateAndNotify(CaptureState.STOPPING); |
| 305 } |
| 306 |
| 307 while (mCaptureState != CaptureState.STOPPED) { |
| 308 try { |
| 309 mCaptureStateLock.wait(); |
| 310 } catch (InterruptedException ex) { |
| 311 Log.e(TAG, "ScreenCaptureEvent: " + ex); |
| 312 } |
| 313 } |
| 314 } |
| 315 } |
| 316 |
| 317 private void createImageReaderWithFormat(int format) { |
| 318 final int maxImages = 2; |
| 319 mImageReader = ImageReader.newInstance(mWidth, mHeight, format, maxImage
s); |
| 320 mSurface = mImageReader.getSurface(); |
| 321 final CrImageReaderListener imageReaderListener = new CrImageReaderListe
ner(); |
| 322 mImageReader.setOnImageAvailableListener(imageReaderListener, mBackgroun
dHandler); |
| 323 } |
| 324 |
| 325 private void createVirtualDisplay() { |
| 326 mVirtualDisplay = mMediaProjection.createVirtualDisplay("ScreenCapture",
mWidth, mHeight, |
| 327 mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mSurface, null, |
| 328 null); |
| 329 } |
| 330 |
| 331 private void changeCaptureStateAndNotify(CaptureState state) { |
| 332 synchronized (mCaptureStateLock) { |
| 333 mCaptureState = state; |
| 334 mCaptureStateLock.notifyAll(); |
| 335 } |
| 336 } |
| 337 |
| 338 // Method for ScreenCapture implementations to call back native code. |
| 339 private native void nativeOnRGBAFrameAvailable(long nativeScreenCaptureMachi
neAndroid, |
| 340 ByteBuffer buf, int left, int top, int width, int height, int rowStr
ide, |
| 341 long timestamp); |
| 342 // Method for ScreenCapture implementations to call back native code. |
| 343 private native void nativeOnI420FrameAvailable(long nativeScreenCaptureMachi
neAndroid, |
| 344 ByteBuffer yBuffer, int yStride, ByteBuffer uBuffer, ByteBuffer vBuf
fer, |
| 345 int uvRowStride, int uvPixelStride, int left, int top, int width, in
t height, |
| 346 long timestamp); |
| 347 // Method for ScreenCapture implementations to call back native code. |
| 348 private native void nativeOnActivityResult( |
| 349 long nativeScreenCaptureMachineAndroid, boolean result); |
| 350 } |
OLD | NEW |