blob: a13f871d7451b0d021ba76b5faa1044014976e10 [file] [log] [blame]
arsanyb75f2542016-08-31 18:50:52 -07001/*
2 * Copyright 2016 The WebRTC project authors. All Rights Reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10
11package org.webrtc;
12
13import android.annotation.TargetApi;
14import android.app.Activity;
15import android.content.Context;
16import android.content.Intent;
17import android.hardware.display.DisplayManager;
18import android.hardware.display.VirtualDisplay;
19import android.media.projection.MediaProjection;
20import android.media.projection.MediaProjectionManager;
21import android.view.Surface;
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010022import javax.annotation.Nullable;
arsanyb75f2542016-08-31 18:50:52 -070023
arsanyb75f2542016-08-31 18:50:52 -070024/**
25 * An implementation of VideoCapturer to capture the screen content as a video stream.
26 * Capturing is done by {@code MediaProjection} on a {@code SurfaceTexture}. We interact with this
27 * {@code SurfaceTexture} using a {@code SurfaceTextureHelper}.
28 * The {@code SurfaceTextureHelper} is created by the native code and passed to this capturer in
29 * {@code VideoCapturer.initialize()}. On receiving a new frame, this capturer passes it
Magnus Jedvert1a759c62018-04-24 15:11:02 +020030 * as a texture to the native code via {@code CapturerObserver.onFrameCaptured()}. This takes
arsanyb75f2542016-08-31 18:50:52 -070031 * place on the HandlerThread of the given {@code SurfaceTextureHelper}. When done with each frame,
32 * the native code returns the buffer to the {@code SurfaceTextureHelper} to be used for new
33 * frames. At any time, at most one frame is being processed.
34 */
35@TargetApi(21)
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020036public class ScreenCapturerAndroid implements VideoCapturer, VideoSink {
sakalb6760f92016-09-29 04:12:44 -070037 private static final int DISPLAY_FLAGS =
38 DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
arsanyb75f2542016-08-31 18:50:52 -070039 // DPI for VirtualDisplay, does not seem to matter for us.
40 private static final int VIRTUAL_DISPLAY_DPI = 400;
41
42 private final Intent mediaProjectionPermissionResultData;
43 private final MediaProjection.Callback mediaProjectionCallback;
44
45 private int width;
46 private int height;
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010047 @Nullable private VirtualDisplay virtualDisplay;
48 @Nullable private SurfaceTextureHelper surfaceTextureHelper;
49 @Nullable private CapturerObserver capturerObserver;
arsanyb75f2542016-08-31 18:50:52 -070050 private long numCapturedFrames = 0;
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010051 @Nullable private MediaProjection mediaProjection;
arsanyb75f2542016-08-31 18:50:52 -070052 private boolean isDisposed = false;
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010053 @Nullable private MediaProjectionManager mediaProjectionManager;
arsanyb75f2542016-08-31 18:50:52 -070054
55 /**
56 * Constructs a new Screen Capturer.
57 *
58 * @param mediaProjectionPermissionResultData the result data of MediaProjection permission
59 * activity; the calling app must validate that result code is Activity.RESULT_OK before
60 * calling this method.
61 * @param mediaProjectionCallback MediaProjection callback to implement application specific
62 * logic in events such as when the user revokes a previously granted capture permission.
63 **/
sakalb6760f92016-09-29 04:12:44 -070064 public ScreenCapturerAndroid(Intent mediaProjectionPermissionResultData,
arsanyb75f2542016-08-31 18:50:52 -070065 MediaProjection.Callback mediaProjectionCallback) {
66 this.mediaProjectionPermissionResultData = mediaProjectionPermissionResultData;
67 this.mediaProjectionCallback = mediaProjectionCallback;
68 }
69
70 private void checkNotDisposed() {
71 if (isDisposed) {
72 throw new RuntimeException("capturer is disposed.");
73 }
74 }
75
76 @Override
Mirko Bonadei12251b62017-11-05 19:35:31 -080077 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
78 @SuppressWarnings("NoSynchronizedMethodCheck")
sakalb6760f92016-09-29 04:12:44 -070079 public synchronized void initialize(final SurfaceTextureHelper surfaceTextureHelper,
Sami Kalliomäkib2daaaa2018-07-06 11:25:25 +020080 final Context applicationContext, final CapturerObserver capturerObserver) {
arsanyb75f2542016-08-31 18:50:52 -070081 checkNotDisposed();
82
83 if (capturerObserver == null) {
84 throw new RuntimeException("capturerObserver not set.");
85 }
86 this.capturerObserver = capturerObserver;
87
88 if (surfaceTextureHelper == null) {
89 throw new RuntimeException("surfaceTextureHelper not set.");
90 }
91 this.surfaceTextureHelper = surfaceTextureHelper;
92
sakalb6760f92016-09-29 04:12:44 -070093 mediaProjectionManager = (MediaProjectionManager) applicationContext.getSystemService(
94 Context.MEDIA_PROJECTION_SERVICE);
arsanyb75f2542016-08-31 18:50:52 -070095 }
96
arsanyb75f2542016-08-31 18:50:52 -070097 @Override
Mirko Bonadei12251b62017-11-05 19:35:31 -080098 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
99 @SuppressWarnings("NoSynchronizedMethodCheck")
sakalb6760f92016-09-29 04:12:44 -0700100 public synchronized void startCapture(
101 final int width, final int height, final int ignoredFramerate) {
arsanyb75f2542016-08-31 18:50:52 -0700102 checkNotDisposed();
103
104 this.width = width;
105 this.height = height;
106
107 mediaProjection = mediaProjectionManager.getMediaProjection(
108 Activity.RESULT_OK, mediaProjectionPermissionResultData);
109
110 // Let MediaProjection callback use the SurfaceTextureHelper thread.
111 mediaProjection.registerCallback(mediaProjectionCallback, surfaceTextureHelper.getHandler());
112
113 createVirtualDisplay();
114 capturerObserver.onCapturerStarted(true);
115 surfaceTextureHelper.startListening(ScreenCapturerAndroid.this);
116 }
117
118 @Override
Mirko Bonadei12251b62017-11-05 19:35:31 -0800119 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
120 @SuppressWarnings("NoSynchronizedMethodCheck")
arsanyb75f2542016-08-31 18:50:52 -0700121 public synchronized void stopCapture() {
122 checkNotDisposed();
123 ThreadUtils.invokeAtFrontUninterruptibly(surfaceTextureHelper.getHandler(), new Runnable() {
124 @Override
125 public void run() {
126 surfaceTextureHelper.stopListening();
127 capturerObserver.onCapturerStopped();
128
129 if (virtualDisplay != null) {
130 virtualDisplay.release();
131 virtualDisplay = null;
132 }
133
134 if (mediaProjection != null) {
135 // Unregister the callback before stopping, otherwise the callback recursively
136 // calls this method.
137 mediaProjection.unregisterCallback(mediaProjectionCallback);
138 mediaProjection.stop();
139 mediaProjection = null;
140 }
141 }
142 });
143 }
144
arsanyb75f2542016-08-31 18:50:52 -0700145 @Override
Mirko Bonadei12251b62017-11-05 19:35:31 -0800146 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
147 @SuppressWarnings("NoSynchronizedMethodCheck")
arsanyb75f2542016-08-31 18:50:52 -0700148 public synchronized void dispose() {
149 isDisposed = true;
150 }
151
arsanyb75f2542016-08-31 18:50:52 -0700152 /**
153 * Changes output video format. This method can be used to scale the output
154 * video, or to change orientation when the captured screen is rotated for example.
155 *
156 * @param width new output video width
157 * @param height new output video height
158 * @param ignoredFramerate ignored
159 */
160 @Override
Mirko Bonadei12251b62017-11-05 19:35:31 -0800161 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
162 @SuppressWarnings("NoSynchronizedMethodCheck")
arsanyb75f2542016-08-31 18:50:52 -0700163 public synchronized void changeCaptureFormat(
164 final int width, final int height, final int ignoredFramerate) {
165 checkNotDisposed();
166
167 this.width = width;
168 this.height = height;
169
170 if (virtualDisplay == null) {
171 // Capturer is stopped, the virtual display will be created in startCaptuer().
172 return;
173 }
174
175 // Create a new virtual display on the surfaceTextureHelper thread to avoid interference
176 // with frame processing, which happens on the same thread (we serialize events by running
177 // them on the same thread).
178 ThreadUtils.invokeAtFrontUninterruptibly(surfaceTextureHelper.getHandler(), new Runnable() {
179 @Override
180 public void run() {
181 virtualDisplay.release();
182 createVirtualDisplay();
183 }
184 });
185 }
186
187 private void createVirtualDisplay() {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200188 surfaceTextureHelper.setTextureSize(width, height);
sakalb6760f92016-09-29 04:12:44 -0700189 virtualDisplay = mediaProjection.createVirtualDisplay("WebRTC_ScreenCapture", width, height,
190 VIRTUAL_DISPLAY_DPI, DISPLAY_FLAGS, new Surface(surfaceTextureHelper.getSurfaceTexture()),
arsanyb75f2542016-08-31 18:50:52 -0700191 null /* callback */, null /* callback handler */);
192 }
193
194 // This is called on the internal looper thread of {@Code SurfaceTextureHelper}.
195 @Override
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200196 public void onFrame(VideoFrame frame) {
arsanyb75f2542016-08-31 18:50:52 -0700197 numCapturedFrames++;
Sami Kalliomäki682dc612018-01-26 11:06:45 +0100198 capturerObserver.onFrameCaptured(frame);
arsanyb75f2542016-08-31 18:50:52 -0700199 }
200
201 @Override
202 public boolean isScreencast() {
203 return true;
204 }
205
206 public long getNumCapturedFrames() {
207 return numCapturedFrames;
208 }
209}