├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── PresentationOnVirtualDisplay.iml ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── andronblog │ │ └── presentationonvirtualdisplay │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── andronblog │ │ └── presentationonvirtualdisplay │ │ ├── CameraView.java │ │ ├── DemoPresentation.java │ │ ├── MainActivity.java │ │ └── RecorderHelper.java │ └── res │ ├── anim │ └── rotator.xml │ ├── layout │ ├── activity_main.xml │ └── my_presentation.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | PresentationOnVirtualDisplay -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.7 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PresentationOnVirtualDisplay.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PresentationOnVirtualDisplay 2 | 3 | PresentationOnVirtualDisplay is a small android application demonstrating: 4 | 5 | * How to show Presentation on android Virtual Display. Git tag: l01_virtualdisplay. [More details](http://www.andronblog.com/dev/how-to-show-presentation-on-android-virtual-display/) 6 | 7 | * How to capture android Presentation by MediaRecorder. Git tag: l02_mediarecorder. [More details](http://www.andronblog.com/dev/how-to-capture-android-presentation-by-mediarecorder/) 8 | 9 | By [Andrei Mandychev](http://www.andronblog.com/dev/) 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.0" 6 | 7 | defaultConfig { 8 | applicationId "com.andronblog.presentationonvirtualdisplay" 9 | minSdkVersion 21 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:support-v4:23.0.0' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/mandrew/android-sdks/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/andronblog/presentationonvirtualdisplay/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.andronblog.presentationonvirtualdisplay; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/andronblog/presentationonvirtualdisplay/CameraView.java: -------------------------------------------------------------------------------- 1 | package com.andronblog.presentationonvirtualdisplay; 2 | 3 | import android.app.Activity; 4 | import android.app.Presentation; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.hardware.Camera; 8 | import android.util.AttributeSet; 9 | import android.util.Log; 10 | import android.view.Display; 11 | import android.view.Surface; 12 | import android.view.SurfaceHolder; 13 | import android.view.SurfaceView; 14 | 15 | import java.io.IOException; 16 | 17 | public class CameraView extends SurfaceView implements SurfaceHolder.Callback { 18 | 19 | private static final String TAG = "CameraView"; 20 | private static final int CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_FRONT; 21 | 22 | private Context mContext; 23 | private SurfaceHolder mHolder; 24 | private Camera mCamera; 25 | 26 | public CameraView(Context context) { 27 | super(context); 28 | initCameraView(context); 29 | } 30 | 31 | public CameraView(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | initCameraView(context); 34 | } 35 | 36 | public CameraView(Context context, AttributeSet attrs, int defStyle) { 37 | super(context, attrs, defStyle); 38 | initCameraView(context); 39 | } 40 | 41 | private void initCameraView(Context context) { 42 | mContext = context; 43 | // TODO check that camera exist 44 | mCamera = getCameraInstance(); 45 | // Install a SurfaceHolder.Callback so we get notified when the 46 | // underlying surface is created and destroyed. 47 | mHolder = getHolder(); 48 | mHolder.addCallback(this); 49 | } 50 | 51 | public void setCameraDisplayOrientation(int displayOrientation) { 52 | 53 | if (mCamera == null) 54 | return; 55 | 56 | android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); 57 | android.hardware.Camera.getCameraInfo(CAMERA_ID, info); 58 | int degrees = 0; 59 | switch (displayOrientation) { 60 | case Surface.ROTATION_0: degrees = 0; break; 61 | case Surface.ROTATION_90: degrees = 90; break; 62 | case Surface.ROTATION_180: degrees = 180; break; 63 | case Surface.ROTATION_270: degrees = 270; break; 64 | } 65 | 66 | int result; 67 | if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 68 | result = (info.orientation + degrees) % 360; 69 | result = (360 - result) % 360; // compensate the mirror 70 | } else { // back-facing 71 | result = (info.orientation - degrees + 360) % 360; 72 | } 73 | mCamera.setDisplayOrientation(result); 74 | } 75 | 76 | public void surfaceCreated(SurfaceHolder holder) { 77 | // The Surface has been created, now tell the camera where to draw the preview. 78 | try { 79 | mCamera.setPreviewDisplay(holder); 80 | mCamera.startPreview(); 81 | } catch (IOException e) { 82 | Log.d(TAG, "Error setting camera preview: " + e.getMessage()); 83 | } 84 | } 85 | 86 | public void surfaceDestroyed(SurfaceHolder holder) { 87 | if (mCamera != null) { 88 | mCamera.release(); 89 | } 90 | } 91 | 92 | public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { 93 | // If your preview can change or rotate, take care of those events here. 94 | // Make sure to stop the preview before resizing or reformatting it. 95 | 96 | if (mHolder.getSurface() == null){ 97 | // preview surface does not exist 98 | return; 99 | } 100 | 101 | // stop preview before making changes 102 | try { 103 | mCamera.stopPreview(); 104 | } catch (Exception e){ 105 | // ignore: tried to stop a non-existent preview 106 | } 107 | 108 | // set preview size and make any resize, rotate or 109 | // reformatting changes here 110 | 111 | // start preview with new settings 112 | try { 113 | mCamera.setPreviewDisplay(mHolder); 114 | mCamera.startPreview(); 115 | 116 | } catch (Exception e){ 117 | Log.d(TAG, "Error starting camera preview: " + e.getMessage()); 118 | } 119 | } 120 | 121 | private boolean checkCameraHardware(Context context) { 122 | if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){ 123 | // this device has a camera 124 | return true; 125 | } else { 126 | // no camera on this device 127 | return false; 128 | } 129 | } 130 | 131 | public static Camera getCameraInstance(){ 132 | Camera c = null; 133 | try { 134 | c = Camera.open(CAMERA_ID); 135 | } 136 | catch (Exception e){ 137 | // Camera is not available (in use or does not exist) 138 | } 139 | return c; // returns null if camera is unavailable 140 | } 141 | 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/andronblog/presentationonvirtualdisplay/DemoPresentation.java: -------------------------------------------------------------------------------- 1 | package com.andronblog.presentationonvirtualdisplay; 2 | 3 | import android.app.Activity; 4 | import android.app.Presentation; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.util.Log; 10 | import android.view.Display; 11 | import android.view.Surface; 12 | import android.view.animation.Animation; 13 | import android.view.animation.AnimationUtils; 14 | import android.widget.TextView; 15 | 16 | public class DemoPresentation extends Presentation { 17 | 18 | private static final String TAG = "DemoPresentation"; 19 | 20 | private int mDefaultDisplayOrientation = Surface.ROTATION_0; 21 | private Handler mTimerHandler; 22 | 23 | public DemoPresentation(Context context, Display display) { 24 | super(context, display); 25 | mTimerHandler = new Handler(); 26 | if (context instanceof Activity) { 27 | Activity activity = (Activity) context; 28 | display = activity.getWindowManager().getDefaultDisplay(); 29 | mDefaultDisplayOrientation = display.getRotation(); 30 | } 31 | } 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | Log.i(TAG, "onCreate"); 37 | setContentView(R.layout.my_presentation); 38 | 39 | TextView tv = (TextView) findViewById(R.id.textView); 40 | Animation myRotation = AnimationUtils.loadAnimation(getContext(), R.anim.rotator); 41 | tv.startAnimation(myRotation); 42 | 43 | CameraView cameraView = (CameraView) findViewById(R.id.cameraView); 44 | cameraView.setCameraDisplayOrientation(mDefaultDisplayOrientation); 45 | 46 | final TextView timeTextView = (TextView) findViewById(R.id.tv_time); 47 | final long startTime = System.currentTimeMillis(); 48 | final Runnable timerRunnable = new Runnable() { 49 | @Override 50 | public void run() { 51 | long millis = System.currentTimeMillis() - startTime; 52 | int seconds = (int) (millis / 1000); 53 | int minutes = seconds / 60; 54 | seconds = seconds % 60; 55 | timeTextView.setText(String.format("%d:%02d", minutes, seconds)); 56 | mTimerHandler.postDelayed(this, 500); 57 | } 58 | }; 59 | mTimerHandler.postDelayed(timerRunnable, 0); 60 | 61 | setOnDismissListener(new OnDismissListener() { 62 | @Override 63 | public void onDismiss(DialogInterface dialogInterface) { 64 | Log.i(TAG, "onDismiss"); 65 | mTimerHandler.removeCallbacks(timerRunnable); 66 | } 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/andronblog/presentationonvirtualdisplay/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.andronblog.presentationonvirtualdisplay; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.Presentation; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Point; 10 | import android.hardware.display.DisplayManager; 11 | import android.hardware.display.VirtualDisplay; 12 | import android.media.MediaPlayer; 13 | import android.media.MediaRecorder; 14 | import android.media.MediaRouter; 15 | import android.media.projection.MediaProjection; 16 | import android.media.projection.MediaProjectionManager; 17 | import android.net.Uri; 18 | import android.os.Build; 19 | import android.os.Bundle; 20 | import android.os.Environment; 21 | import android.support.v4.app.ActivityCompat; 22 | import android.support.v4.content.ContextCompat; 23 | import android.util.DisplayMetrics; 24 | import android.util.Log; 25 | import android.util.Size; 26 | import android.view.Display; 27 | import android.view.Gravity; 28 | import android.view.Menu; 29 | import android.view.MenuItem; 30 | import android.view.Surface; 31 | import android.view.SurfaceView; 32 | import android.view.View; 33 | import android.view.WindowManager; 34 | import android.view.animation.Animation; 35 | import android.view.animation.AnimationUtils; 36 | import android.widget.Button; 37 | import android.widget.TextView; 38 | import android.widget.Toast; 39 | 40 | import java.io.IOException; 41 | 42 | public class MainActivity extends Activity { 43 | 44 | private static final String TAG = "MainActivity"; 45 | 46 | private static final int SCREEN_CAPTURE_PERMISSION_CODE = 1; 47 | private static final int EXTERNAL_STORAGE_PERMISSION_CODE = 2; 48 | 49 | private static final int FRAMERATE = 30; 50 | private static final String FILENAME = Environment.getExternalStorageDirectory().getPath()+"/presentation.mp4"; 51 | 52 | private int mWidth; 53 | private int mHeight; 54 | private DisplayMetrics mMetrics = new DisplayMetrics(); 55 | 56 | private DisplayManager mDisplayManager; 57 | private VirtualDisplay mVirtualDisplay; 58 | private MediaRecorder mMediaRecorder; 59 | 60 | private int mResultCode; 61 | private Intent mResultData; 62 | 63 | private MediaProjectionManager mProjectionManager; 64 | private MediaProjection mProjection; 65 | private MediaProjection.Callback mProjectionCallback; 66 | 67 | private MediaPlayer mMediaPlayer; 68 | private SurfaceView mSurfaceView; 69 | 70 | private Surface mSurface; 71 | private Button mButtonCreate; 72 | private Button mButtonDestroy; 73 | private Button mButtonPlayVideo; 74 | private Button mButtonStopVideo; 75 | 76 | @Override 77 | protected void onCreate(Bundle savedInstanceState) { 78 | super.onCreate(savedInstanceState); 79 | setContentView(R.layout.activity_main); 80 | 81 | mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView); 82 | mSurface = mSurfaceView.getHolder().getSurface(); 83 | 84 | // Obtain display metrics of current display to know its density (dpi) 85 | WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 86 | Display display = wm.getDefaultDisplay(); 87 | display.getMetrics(mMetrics); 88 | // Initialize resolution of virtual display in pixels to show 89 | // the surface view on full screen 90 | mWidth = mSurfaceView.getLayoutParams().width; 91 | mHeight = mSurfaceView.getLayoutParams().height; 92 | 93 | mDisplayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); 94 | mDisplayManager.registerDisplayListener(mDisplayListener, null); 95 | mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); 96 | mMediaRecorder = new MediaRecorder(); 97 | 98 | mButtonCreate = (Button) findViewById(R.id.btn_create_virtual_display); 99 | mButtonCreate.setEnabled(false); 100 | mButtonCreate.setOnClickListener(new View.OnClickListener() { 101 | @Override 102 | public void onClick(View view) { 103 | startScreenCapture(); 104 | } 105 | }); 106 | 107 | mButtonDestroy = (Button) findViewById(R.id.btn_destroy_virtual_display); 108 | mButtonDestroy.setEnabled(false); 109 | mButtonDestroy.setOnClickListener(new View.OnClickListener() { 110 | @Override 111 | public void onClick(View view) { 112 | stopScreenCapture(); 113 | } 114 | }); 115 | 116 | mButtonPlayVideo = (Button) findViewById(R.id.btn_play); 117 | mButtonPlayVideo.setEnabled(false); 118 | mButtonPlayVideo.setOnClickListener(new View.OnClickListener() { 119 | @Override 120 | public void onClick(View view) { 121 | if (mMediaPlayer == null) { 122 | Uri uri = Uri.parse(FILENAME); 123 | mMediaPlayer = MediaPlayer.create(MainActivity.this, uri, mSurfaceView.getHolder()); 124 | } else { 125 | try { 126 | mMediaPlayer.prepare(); 127 | } catch (IOException e) { 128 | e.printStackTrace(); 129 | return; 130 | } 131 | } 132 | mMediaPlayer.start(); 133 | mButtonCreate.setEnabled(false); 134 | mButtonDestroy.setEnabled(false); 135 | mButtonPlayVideo.setEnabled(false); 136 | mButtonStopVideo.setEnabled(true); 137 | } 138 | }); 139 | 140 | mButtonStopVideo = (Button) findViewById(R.id.btn_stop); 141 | mButtonStopVideo.setEnabled(false); 142 | mButtonStopVideo.setOnClickListener(new View.OnClickListener() { 143 | @Override 144 | public void onClick(View view) { 145 | mMediaPlayer.stop(); 146 | mButtonCreate.setEnabled(true); 147 | mButtonDestroy.setEnabled(false); 148 | mButtonPlayVideo.setEnabled(true); 149 | mButtonStopVideo.setEnabled(false); 150 | } 151 | }); 152 | 153 | // Check if we have write permission 154 | int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); 155 | if (permission != PackageManager.PERMISSION_GRANTED) { 156 | Log.w(TAG, "Write permissions is not granted"); 157 | // Request permissions 158 | ActivityCompat.requestPermissions(this, 159 | new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 160 | EXTERNAL_STORAGE_PERMISSION_CODE); 161 | } else { 162 | Log.i(TAG, "Write permission is granted!"); 163 | mButtonCreate.setEnabled(true); 164 | } 165 | } 166 | 167 | @Override 168 | public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { 169 | switch(requestCode) { 170 | case EXTERNAL_STORAGE_PERMISSION_CODE: { 171 | // If request is cancelled, the result arrays are empty. 172 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 173 | Log.i(TAG, "Write permission is granted!"); 174 | mButtonCreate.setEnabled(true); 175 | } else { 176 | Toast.makeText(this, "Write permission is not granted", Toast.LENGTH_LONG).show(); 177 | } 178 | return; 179 | } 180 | } 181 | } 182 | 183 | @Override 184 | protected void onStart() { 185 | super.onStart(); 186 | Log.d(TAG, "onStart"); 187 | } 188 | 189 | @Override 190 | protected void onResume() { 191 | super.onResume(); 192 | Log.d(TAG, "onResume"); 193 | } 194 | 195 | @Override 196 | protected void onPause() { 197 | super.onPause(); 198 | Log.d(TAG, "onPause"); 199 | destroyVirtualDisplay(); 200 | if (mMediaPlayer != null) { 201 | mMediaPlayer.release(); 202 | mMediaPlayer = null; 203 | mButtonPlayVideo.setEnabled(false); 204 | mButtonStopVideo.setEnabled(false); 205 | } 206 | } 207 | 208 | @Override 209 | protected void onStop() { 210 | super.onStop(); 211 | Log.d(TAG, "onStop"); 212 | } 213 | 214 | @Override 215 | protected void onDestroy() { 216 | super.onDestroy(); 217 | Log.d(TAG, "onDestroy"); 218 | mDisplayManager.unregisterDisplayListener(mDisplayListener); 219 | if (mProjection != null) { 220 | Log.i(TAG, "Stop media projection"); 221 | mProjection.unregisterCallback(mProjectionCallback); 222 | mProjection.stop(); 223 | mProjection = null; 224 | } 225 | mMediaRecorder.release(); 226 | } 227 | 228 | private void startScreenCapture() { 229 | if (mProjection != null) { 230 | // start virtual display 231 | Log.i(TAG, "The media projection is already gotten"); 232 | createVirtualDisplay(); 233 | } else if (mResultCode != 0 && mResultData != null) { 234 | // get media projection 235 | Log.i(TAG, "Get media projection with the existing permission"); 236 | mProjection = getProjection(); 237 | createVirtualDisplay(); 238 | } else { 239 | Log.i(TAG, "Request the permission for media projection"); 240 | startActivityForResult(mProjectionManager.createScreenCaptureIntent(), SCREEN_CAPTURE_PERMISSION_CODE); 241 | } 242 | } 243 | 244 | private void stopScreenCapture() { 245 | destroyVirtualDisplay(); 246 | } 247 | 248 | @Override 249 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 250 | mResultCode = resultCode; 251 | mResultData = data; 252 | if (requestCode != SCREEN_CAPTURE_PERMISSION_CODE) { 253 | Toast.makeText(this, "Unknown request code: " + requestCode, Toast.LENGTH_SHORT).show(); 254 | return; 255 | } 256 | if (resultCode != RESULT_OK) { 257 | Toast.makeText(this, "Screen Cast Permission Denied", Toast.LENGTH_SHORT).show(); 258 | return; 259 | } 260 | Log.i(TAG, "Get media projection with the new permission"); 261 | mProjection = getProjection(); 262 | createVirtualDisplay(); 263 | } 264 | 265 | private MediaProjection getProjection() { 266 | MediaProjection projection = mProjectionManager.getMediaProjection(mResultCode, mResultData); 267 | // Add a callback to be informed if the projection 268 | // will be stopped from the status bar. 269 | mProjectionCallback = new MediaProjection.Callback() { 270 | @Override 271 | public void onStop() { 272 | Log.d(TAG, "MediaProjection.Callback onStop obj:" + toString()); 273 | destroyVirtualDisplay(); 274 | mProjection = null; 275 | } 276 | }; 277 | projection.registerCallback(mProjectionCallback, null); 278 | return projection; 279 | } 280 | 281 | private void createVirtualDisplay() { 282 | if (mProjection != null && mVirtualDisplay == null) { 283 | Log.d(TAG, "createVirtualDisplay WxH (px): " + mWidth + "x" + mHeight + 284 | ", dpi: " + mMetrics.densityDpi); 285 | if (!prepareMediaRecorder(mWidth, mHeight, FRAMERATE, FILENAME)) { 286 | Toast.makeText(this, "Can't prepare MediaRecorder", Toast.LENGTH_LONG).show(); 287 | return; 288 | } 289 | int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; 290 | //flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; 291 | mVirtualDisplay = mProjection.createVirtualDisplay("MyVirtualDisplay", 292 | mWidth, mHeight, mMetrics.densityDpi, flags, mMediaRecorder.getSurface(), 293 | null /*Callbacks*/, null /*Handler*/); 294 | mButtonCreate.setEnabled(false); 295 | mButtonDestroy.setEnabled(true); 296 | mButtonPlayVideo.setEnabled(false); 297 | // Release the previous instance of media player before recording new data into the same file. 298 | if (mMediaPlayer != null) { 299 | mMediaPlayer.release(); 300 | mMediaPlayer = null; 301 | } 302 | // Start recording the content of MediaRecorder surface rendering by VirtualDisplay 303 | // into file. 304 | mMediaRecorder.start(); 305 | } 306 | } 307 | 308 | private void destroyVirtualDisplay() { 309 | Log.d(TAG, "destroyVirtualDisplay"); 310 | if (mVirtualDisplay != null) { 311 | Log.d(TAG, "destroyVirtualDisplay release"); 312 | mVirtualDisplay.release(); 313 | mVirtualDisplay = null; 314 | mMediaRecorder.stop(); 315 | } 316 | mButtonDestroy.setEnabled(false); 317 | mButtonCreate.setEnabled(true); 318 | mButtonPlayVideo.setEnabled(true); 319 | } 320 | 321 | private boolean prepareMediaRecorder(int width, int height, int framerate, String filename) { 322 | Size sz = new Size(width, height); 323 | boolean supported = RecorderHelper.isSupportedByAVCEncoder(sz, framerate); 324 | if (!supported) { 325 | Log.e(TAG, "The combination of video size and framerate is not supported by MediaCodec"); 326 | return false; 327 | } 328 | 329 | mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); 330 | mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); 331 | mMediaRecorder.setVideoEncodingBitRate(RecorderHelper.getVideoBitRate(sz)); 332 | mMediaRecorder.setVideoFrameRate(framerate); 333 | mMediaRecorder.setVideoSize(sz.getWidth(), sz.getHeight()); 334 | mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 335 | mMediaRecorder.setOutputFile(filename); 336 | try { 337 | mMediaRecorder.prepare(); 338 | } catch (IOException e) { 339 | e.printStackTrace(); 340 | Log.e(TAG, "Prepare MediaRecorder is failed"); 341 | return false; 342 | } 343 | 344 | return true; 345 | } 346 | 347 | private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { 348 | 349 | private boolean mNewDisplayAdded = false; 350 | private int mCurrentDisplayId = -1; 351 | private DemoPresentation mPresentation; 352 | 353 | @Override 354 | public void onDisplayAdded(int i) { 355 | Log.d(TAG, "onDisplayAdded id=" + i); 356 | if (!mNewDisplayAdded && mCurrentDisplayId == -1) { 357 | mNewDisplayAdded = true; 358 | mCurrentDisplayId = i; 359 | } 360 | } 361 | 362 | @Override 363 | public void onDisplayRemoved(int i) { 364 | Log.d(TAG, "onDisplayRemoved id=" + i); 365 | if (mCurrentDisplayId == i) { 366 | mNewDisplayAdded = false; 367 | mCurrentDisplayId = -1; 368 | if (mPresentation != null) { 369 | mPresentation.dismiss(); 370 | mPresentation = null; 371 | } 372 | } 373 | } 374 | 375 | @Override 376 | public void onDisplayChanged(int i) { 377 | Log.d(TAG, "onDisplayChanged id=" + i); 378 | if (mCurrentDisplayId == i) { 379 | if (mNewDisplayAdded) { 380 | // create a presentation 381 | mNewDisplayAdded = false; 382 | Display display = mDisplayManager.getDisplay(i); 383 | mPresentation = new DemoPresentation(MainActivity.this, display); 384 | mPresentation.show(); 385 | } 386 | } 387 | } 388 | }; 389 | } 390 | -------------------------------------------------------------------------------- /app/src/main/java/com/andronblog/presentationonvirtualdisplay/RecorderHelper.java: -------------------------------------------------------------------------------- 1 | package com.andronblog.presentationonvirtualdisplay; 2 | 3 | import android.media.MediaCodecInfo; 4 | import android.media.MediaCodecList; 5 | import android.util.Log; 6 | import android.util.Size; 7 | 8 | public class RecorderHelper { 9 | 10 | private final static String TAG = "MediaRecorderHelper"; 11 | private final static boolean VERBOSE = true; 12 | 13 | private static final int BIT_RATE_1080P = 16000000; 14 | private static final int BIT_RATE_MIN = 64000; 15 | private static final int BIT_RATE_MAX = 40000000; 16 | 17 | /** 18 | * Calculate a video bit rate based on the size. The bit rate is scaled 19 | * based on ratio of video size to 1080p size. 20 | */ 21 | public static int getVideoBitRate(Size sz) { 22 | int rate = BIT_RATE_1080P; 23 | float scaleFactor = sz.getHeight() * sz.getWidth() / (float)(1920 * 1080); 24 | rate = (int)(rate * scaleFactor); 25 | 26 | // Clamp to the MIN, MAX range. 27 | return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate)); 28 | } 29 | 30 | /** 31 | * Check if encoder can support this size and frame rate combination by querying 32 | * MediaCodec capability. Check is based on size and frame rate. Ignore the bit rate 33 | * as the bit rates targeted in this test are well below the bit rate max value specified 34 | * by AVC specification for certain level. 35 | */ 36 | public static boolean isSupportedByAVCEncoder(Size sz, int frameRate) { 37 | String mimeType = "video/avc"; 38 | MediaCodecInfo codecInfo = getEncoderInfo(mimeType); 39 | if (codecInfo == null) { 40 | return false; 41 | } 42 | MediaCodecInfo.CodecCapabilities cap = codecInfo.getCapabilitiesForType(mimeType); 43 | if (cap == null) { 44 | return false; 45 | } 46 | 47 | int highestLevel = 0; 48 | for (MediaCodecInfo.CodecProfileLevel lvl : cap.profileLevels) { 49 | if (lvl.level > highestLevel) { 50 | highestLevel = lvl.level; 51 | } 52 | } 53 | 54 | if(VERBOSE) { 55 | Log.v(TAG, "The highest level supported by encoder is: " + highestLevel); 56 | } 57 | 58 | // Put bitRate here for future use. 59 | int maxW, maxH, bitRate; 60 | // Max encoding speed. 61 | int maxMacroblocksPerSecond = 0; 62 | switch(highestLevel) { 63 | case MediaCodecInfo.CodecProfileLevel.AVCLevel21: 64 | maxW = 352; 65 | maxH = 576; 66 | bitRate = 4000000; 67 | maxMacroblocksPerSecond = 19800; 68 | break; 69 | case MediaCodecInfo.CodecProfileLevel.AVCLevel22: 70 | maxW = 720; 71 | maxH = 480; 72 | bitRate = 4000000; 73 | maxMacroblocksPerSecond = 20250; 74 | break; 75 | case MediaCodecInfo.CodecProfileLevel.AVCLevel3: 76 | maxW = 720; 77 | maxH = 480; 78 | bitRate = 10000000; 79 | maxMacroblocksPerSecond = 40500; 80 | break; 81 | case MediaCodecInfo.CodecProfileLevel.AVCLevel31: 82 | maxW = 1280; 83 | maxH = 720; 84 | bitRate = 14000000; 85 | maxMacroblocksPerSecond = 108000; 86 | break; 87 | case MediaCodecInfo.CodecProfileLevel.AVCLevel32: 88 | maxW = 1280; 89 | maxH = 720; 90 | bitRate = 20000000; 91 | maxMacroblocksPerSecond = 216000; 92 | break; 93 | case MediaCodecInfo.CodecProfileLevel.AVCLevel4: 94 | maxW = 1920; 95 | maxH = 1088; // It should be 1088 in terms of AVC capability. 96 | bitRate = 20000000; 97 | maxMacroblocksPerSecond = 245760; 98 | break; 99 | case MediaCodecInfo.CodecProfileLevel.AVCLevel41: 100 | maxW = 1920; 101 | maxH = 1088; // It should be 1088 in terms of AVC capability. 102 | bitRate = 50000000; 103 | maxMacroblocksPerSecond = 245760; 104 | break; 105 | case MediaCodecInfo.CodecProfileLevel.AVCLevel42: 106 | maxW = 2048; 107 | maxH = 1088; // It should be 1088 in terms of AVC capability. 108 | bitRate = 50000000; 109 | maxMacroblocksPerSecond = 522240; 110 | break; 111 | case MediaCodecInfo.CodecProfileLevel.AVCLevel5: 112 | maxW = 3672; 113 | maxH = 1536; 114 | bitRate = 135000000; 115 | maxMacroblocksPerSecond = 589824; 116 | break; 117 | case MediaCodecInfo.CodecProfileLevel.AVCLevel51: 118 | default: 119 | maxW = 4096; 120 | maxH = 2304; 121 | bitRate = 240000000; 122 | maxMacroblocksPerSecond = 983040; 123 | break; 124 | } 125 | 126 | // Check size limit. 127 | if (sz.getWidth() > maxW || sz.getHeight() > maxH) { 128 | Log.i(TAG, "Requested resolution " + sz.toString() + " exceeds (" + 129 | maxW + "," + maxH + ")"); 130 | return false; 131 | } 132 | 133 | // Check frame rate limit. 134 | Size sizeInMb = new Size((sz.getWidth() + 15) / 16, (sz.getHeight() + 15) / 16); 135 | int maxFps = maxMacroblocksPerSecond / (sizeInMb.getWidth() * sizeInMb.getHeight()); 136 | if (frameRate > maxFps) { 137 | Log.i(TAG, "Requested frame rate " + frameRate + " exceeds " + maxFps); 138 | return false; 139 | } 140 | 141 | return true; 142 | } 143 | 144 | 145 | private static MediaCodecInfo getEncoderInfo(String mimeType) { 146 | int numCodecs = MediaCodecList.getCodecCount(); 147 | for (int i = 0; i < numCodecs; i++) { 148 | MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); 149 | 150 | if (!codecInfo.isEncoder()) { 151 | continue; 152 | } 153 | 154 | String[] types = codecInfo.getSupportedTypes(); 155 | for (int j = 0; j < types.length; j++) { 156 | if (types[j].equalsIgnoreCase(mimeType)) { 157 | return codecInfo; 158 | } 159 | } 160 | } 161 | return null; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/res/anim/rotator.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 15 | 22 | 23 |