/* HostDeviceScreenCast.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.host.recorder.screen; import android.annotation.TargetApi; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.Image; import android.media.ImageReader; import android.media.projection.MediaProjection; import android.media.projection.MediaProjectionManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.view.WindowManager; import org.deviceconnect.android.deviceplugin.host.recorder.HostDevicePhotoRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.HostDevicePreviewServer; import org.deviceconnect.android.deviceplugin.host.recorder.util.MixedReplaceMediaServer; import org.deviceconnect.android.provider.FileManager; import java.io.ByteArrayOutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import static android.R.attr.max; /** * Host Device Screen Cast. * * @author NTT DOCOMO, INC. */ @TargetApi(21) public class HostDeviceScreenCast extends HostDevicePreviewServer implements HostDevicePhotoRecorder { static final String RESULT_DATA = "result_data"; static final String EXTRA_CALLBACK = "callback"; private static final String ID = "screen"; private static final String NAME = "AndroidHost Screen"; private static final String MIME_TYPE = "video/x-mjpeg"; private static final double DEFAULT_MAX_FPS = 10.0d; /** ファイル名に付けるプレフィックス. */ private static final String FILENAME_PREFIX = "android_screen_"; /** ファイルの拡張子. */ private static final String FILE_EXTENSION = ".jpg"; /** 日付のフォーマット. */ private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("yyyyMMdd_kkmmss", Locale.JAPAN); /** * マイムタイプ一覧を定義. */ private List<String> mMimeTypes = new ArrayList<String>() { { add(MIME_TYPE); } }; private final Context mContext; private final int mDisplayDensityDpi; private final Object mLockObj = new Object(); private final Logger mLogger = Logger.getLogger("host.dplugin"); private MixedReplaceMediaServer mServer; private MediaProjectionManager mManager; private MediaProjection mMediaProjection; private VirtualDisplay mVirtualDisplay; private ImageReader mImageReader; private FileManager mFileMgr; private boolean mIsCasting; private Thread mThread; private final List<PictureSize> mSupportedPreviewSizes = new ArrayList<>(); private final List<PictureSize> mSupportedPictureSizes = new ArrayList<>(); private PictureSize mPreviewSize; private PictureSize mPictureSize; private long mFrameInterval; private double mMaxFps; private RecorderState mState; private Handler mHandler = new Handler(Looper.getMainLooper()); private BroadcastReceiver mConfigChangeReceiver; public HostDeviceScreenCast(final Context context, final FileManager fileMgr) { super(context, 2000); mContext = context; mFileMgr = fileMgr; mManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); DisplayMetrics metrics = context.getResources().getDisplayMetrics(); PictureSize size = new PictureSize(metrics.widthPixels, metrics.heightPixels); mDisplayDensityDpi = metrics.densityDpi; initSupportedPreviewSizes(size); mState = RecorderState.INACTTIVE; mMaxFps = DEFAULT_MAX_FPS; setMaxFrameRate(mMaxFps); } private void initSupportedPreviewSizes(final PictureSize originalSize) { final int num = 4; final int w = originalSize.getWidth(); final int h = originalSize.getHeight(); mSupportedPreviewSizes.clear(); for (int i = 1; i <= num; i++) { float scale = i / ((float) num); PictureSize previewSize = new PictureSize((int) (w * scale), (int) (h * scale)); mSupportedPreviewSizes.add(previewSize); mSupportedPictureSizes.add(previewSize); } mPreviewSize = mSupportedPreviewSizes.get(0); mPictureSize = mSupportedPictureSizes.get(num - 1); } @Override public void initialize() { // Nothing to do. } @Override public void clean() { stopWebServer(); if (mMediaProjection != null) { mMediaProjection.stop(); mMediaProjection = null; } } @Override public String getId() { return ID; } @Override public String getName() { return NAME; } @Override public String getMimeType() { return MIME_TYPE; } @Override public RecorderState getState() { return mState; } @Override public PictureSize getPictureSize() { return mPictureSize; } @Override public void setPictureSize(final PictureSize size) { mPictureSize = size; } @Override public PictureSize getPreviewSize() { return mPreviewSize; } @Override public synchronized void setPreviewSize(final PictureSize size) { mPreviewSize = size; if (mIsCasting) { restartScreenCast(); } } @Override public double getMaxFrameRate() { return mMaxFps; } @Override public void setMaxFrameRate(double frameRate) { mMaxFps = frameRate; mFrameInterval = 1000L / max; } @Override public List<PictureSize> getSupportedPreviewSizes() { return mSupportedPreviewSizes; } @Override public List<PictureSize> getSupportedPictureSizes() { return mSupportedPictureSizes; } @Override public List<String> getSupportedMimeTypes() { return mMimeTypes; } @Override public boolean isSupportedPictureSize(int width, int height) { if (mSupportedPictureSizes != null) { for (PictureSize size : mSupportedPictureSizes) { if (width == size.getWidth() && height == size.getHeight()) { return true; } } } return false; } @Override public boolean isSupportedPreviewSize(int width, int height) { if (mSupportedPreviewSizes != null) { for (PictureSize size : mSupportedPreviewSizes) { if (width == size.getWidth() && height == size.getHeight()) { return true; } } } return false; } @Override public boolean isBack() { return false; } @Override public void turnOnFlashLight() { } @Override public void turnOffFlashLight() { } @Override public boolean isFlashLightState() { return false; } @Override public boolean isUseFlashLight() { return false; } @Override public void startWebServer(final OnWebServerStartCallback callback) { mLogger.info("Starting web server..."); synchronized (mLockObj) { if (mServer == null) { mServer = new MixedReplaceMediaServer(); mServer.setServerName("HostDevicePlugin ScreenCast Server"); mServer.setContentType(MIME_TYPE); final String ip = mServer.start(); requestPermission(new PermissionCallback() { @Override public void onAllowed() { sendNotification(); startScreenCast(); registerConfigChangeReceiver(); callback.onStart(ip); } @Override public void onDisallowed() { stopWebServer(); callback.onFail(); } }); } else { callback.onStart(mServer.getUrl()); } } mLogger.info("Started web server."); } @Override public void stopWebServer() { mLogger.info("Stopping web server..."); synchronized (mLockObj) { hideNotification(); stopScreenCast(); unregisterConfigChangeReceiver(); if (mServer != null) { mServer.stop(); mServer = null; } } mLogger.info("Stopped web server."); } @Override public void takePhoto(final OnPhotoEventListener listener) { mState = RecorderState.RECORDING; if (!mIsCasting) { requestPermission(new PermissionCallback() { @Override public void onAllowed() { takePhoto(listener, null); } @Override public void onDisallowed() { mState = RecorderState.INACTTIVE; listener.onFailedTakePhoto(); } }); } else { stopScreenCast(); mHandler.postDelayed(new Runnable() { @Override public void run() { takePhoto(listener, new FinishCallback() { @Override public void onFinish() { mHandler.postDelayed(new Runnable() { public void run() { startScreenCast(); } }, 500); } }); } }, 500); } } private void takePhoto(final OnPhotoEventListener listener, final FinishCallback callback) { setupVirtualDisplay(mPictureSize, new VirtualDisplay.Callback() { @Override public void onPaused() { } @Override public void onResumed() { new Thread(new Runnable() { @Override public void run() { takePhotoInternal(new OnPhotoEventListener() { @Override public void onTakePhoto(final String uri, final String filePath) { listener.onTakePhoto(uri, filePath); releaseVirtualDisplay(); } @Override public void onFailedTakePhoto() { listener.onFailedTakePhoto(); releaseVirtualDisplay(); } }); } }).start(); } @Override public void onStopped() { if (callback != null) { callback.onFinish(); } } }); } private void takePhotoInternal(final OnPhotoEventListener listener) { long t = System.currentTimeMillis(); Bitmap bitmap = getScreenshot(); while (bitmap == null && (System.currentTimeMillis() - t) < 5000) { bitmap = getScreenshot(); if (bitmap == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } if (bitmap == null) { mState = RecorderState.INACTTIVE; listener.onFailedTakePhoto(); return; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] media = baos.toByteArray(); if (media == null) { mState = RecorderState.INACTTIVE; listener.onFailedTakePhoto(); return; } // 常に新しいファイル名になるため重複はない。そのため、Overwriteフラグをtrueにする。 mFileMgr.saveFile(createNewFileName(), media, true, new FileManager.SaveFileCallback() { @Override public void onSuccess(@NonNull final String uri) { mState = RecorderState.INACTTIVE; listener.onTakePhoto(uri, null); } @Override public void onFail(@NonNull final Throwable throwable) { mState = RecorderState.INACTTIVE; listener.onFailedTakePhoto(); } }); } private String createNewFileName() { return FILENAME_PREFIX + mSimpleDateFormat.format(new Date()) + FILE_EXTENSION; } private void requestPermission(final PermissionCallback callback) { if (mMediaProjection != null) { callback.onAllowed(); } else { Intent intent = new Intent(); intent.setClass(mContext, PermissionReceiverActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(EXTRA_CALLBACK, new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override protected void onReceiveResult(final int resultCode, final Bundle resultData) { if (resultCode == Activity.RESULT_OK) { Intent data = resultData.getParcelable(RESULT_DATA); if (data != null) { mMediaProjection = mManager.getMediaProjection(resultCode, data); mMediaProjection.registerCallback(new MediaProjection.Callback() { @Override public void onStop() { clean(); } }, new Handler(Looper.getMainLooper())); } } if (mMediaProjection != null) { callback.onAllowed(); } else { callback.onDisallowed(); } } }); mContext.startActivity(intent); } } private void setupVirtualDisplay() { setupVirtualDisplay(mPreviewSize, new VirtualDisplay.Callback() { @Override public void onPaused() { } @Override public void onResumed() { } @Override public void onStopped() { } }); } private void setupVirtualDisplay(final PictureSize size, final VirtualDisplay.Callback callback) { int w = size.getWidth(); int h = size.getHeight(); WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); if (dm.widthPixels > dm.heightPixels) { if (w < h) { w = size.getHeight(); h = size.getWidth(); } } else { if (w > h) { w = size.getHeight(); h = size.getWidth(); } } mImageReader = ImageReader.newInstance(w, h, PixelFormat.RGBA_8888, 4); mVirtualDisplay = mMediaProjection.createVirtualDisplay( "Android Host Screen", w, h, mDisplayDensityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), callback, null); } private void releaseVirtualDisplay() { if (mVirtualDisplay != null) { mVirtualDisplay.release(); mVirtualDisplay = null; } if (mImageReader != null) { mImageReader.setOnImageAvailableListener(null, null); mImageReader.close(); mImageReader = null; } } private void startScreenCast() { if (mIsCasting) { mLogger.info("MediaProjection is already running."); return; } mIsCasting = true; setupVirtualDisplay(); mThread = new Thread(new Runnable() { @Override public void run() { mLogger.info("Server URL: " + mServer.getUrl()); try { while (mIsCasting) { long start = System.currentTimeMillis(); Bitmap bitmap = getScreenshot(); if (bitmap == null) { continue; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] media = baos.toByteArray(); mServer.offerMedia(media); long end = System.currentTimeMillis(); long interval = mFrameInterval - (end - start); if (interval > 0) { Thread.sleep(interval); } } } catch (Throwable e) { mLogger.warning("MediaProjection is broken." + e.getMessage()); stopWebServer(); } } }); mThread.start(); } private void stopScreenCast() { if (!mIsCasting) { return; } mIsCasting = false; if (mThread != null) { mThread.interrupt(); mThread = null; } releaseVirtualDisplay(); } private void restartScreenCast() { stopScreenCast(); startScreenCast(); } private synchronized Bitmap getScreenshot() { Image image = mImageReader.acquireLatestImage(); if (image == null) { return null; } return decodeToBitmap(image); } private Bitmap decodeToBitmap(final Image img) { Image.Plane[] planes = img.getPlanes(); if (planes[0].getBuffer() == null) { return null; } int width = img.getWidth(); int height = img.getHeight(); int pixelStride = planes[0].getPixelStride(); int rowStride = planes[0].getRowStride(); int rowPadding = rowStride - pixelStride * width; Bitmap bitmap = Bitmap.createBitmap( width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(planes[0].getBuffer()); img.close(); return Bitmap.createBitmap(bitmap, 0, 0, width, height, null, true); } private void registerConfigChangeReceiver() { mConfigChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { restartScreenCast(); } }; IntentFilter filter = new IntentFilter( "android.intent.action.CONFIGURATION_CHANGED"); mContext.registerReceiver(mConfigChangeReceiver, filter); } private void unregisterConfigChangeReceiver() { if (mConfigChangeReceiver != null) { mContext.unregisterReceiver(mConfigChangeReceiver); mConfigChangeReceiver = null; } } private interface PermissionCallback { void onAllowed(); void onDisallowed(); } private interface FinishCallback { void onFinish(); } }