diff --git a/Device-Screen-Sharing-Java/.gitignore b/Device-Screen-Sharing-Java/.gitignore new file mode 100644 index 00000000..93e62f94 --- /dev/null +++ b/Device-Screen-Sharing-Java/.gitignore @@ -0,0 +1,15 @@ +# intellij +*.iml + +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild +app/build + +.settings/ +app/jniLibs/ diff --git a/Device-Screen-Sharing-Java/README.md b/Device-Screen-Sharing-Java/README.md new file mode 100644 index 00000000..a2cd565c --- /dev/null +++ b/Device-Screen-Sharing-Java/README.md @@ -0,0 +1,143 @@ +# Screen Sharing + +This app shows how to use `WebView` as the source for screen-sharing video. + +> Check [Basic-Video-Capturer-Camera-2](../Basic-Video-Capturer-Camera-2) project to see how a device camera can be used as the video source for the custom `Capturer`. +## Screen sharing + +Custom video capturer is using `WebView` from the Android application as the source of +a published stream. + +When the app starts up, the `onCreate` method instantiates a `WebView` object: + +```java +webViewContainer = findViewById(R.id.webview); +``` + +Upon connecting to the OpenTok session, the app instantiates a `Publisher` object, and calls its +`setCapturer` method to use a custom video capturer, defined by the `ScreenSharingCapturer` +class: + +```java +@Override +public void onConnected(Session session) { + ScreenSharingCapturer screenSharingCapturer = new ScreenSharingCapturer(MainActivity.this, webViewContainer); + + publisher = new Publisher.Builder(MainActivity.this) + .capturer(screenSharingCapturer) + .build(); + + publisher.setPublisherListener(publisherListener); + publisher.setPublisherVideoType(PublisherKit.PublisherKitVideoType.PublisherKitVideoTypeScreen); + publisher.setAudioFallbackEnabled(false); + + webViewContainer.setWebViewClient(new WebViewClient()); + WebSettings webSettings = webViewContainer.getSettings(); + webSettings.setJavaScriptEnabled(true); + webViewContainer.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + webViewContainer.loadUrl("https://www.tokbox.com"); + + publisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL); + publisherViewContainer.addView(publisher.getView()); + + session.publish(publisher); +} +``` + +> Note: that the call to the `setPublisherVideoType` method sets the video type of the published +stream to `PublisherKitVideoType.PublisherKitVideoTypeScreen`. This optimizes the video encoding for +screen sharing. It is recommended to use a low frame rate (15 frames per second or lower) with this +video type. When using the screen video type in a session that uses the [OpenTok Media +Server](https://tokbox.com/opentok/tutorials/create-session/#media-mode), the +audio-only fallback feature is disabled, so that the video does not drop out in subscribers. + +The `onConnected` method also calls the `loadScreenWebView` method. This method +configures the WebView object, loading the TokBox URL. + +Note that the `webViewContainer` object is passed into the `ScreenSharingCapturer` constructor, +which assigns it to the `contentView` property. + +The `getCaptureSettings` method initializes capture settings to be used by the custom +video capturer: + +```java +@Override +public CaptureSettings getCaptureSettings() { + + CaptureSettings captureSettings = new CaptureSettings(); + captureSettings.fps = fps; + captureSettings.width = width; + captureSettings.height = height; + captureSettings.format = ARGB; + return captureSettings; +} +``` + +The `startCapture` method starts the `frameProducer` thread after 1/15 second: + +```java +@Override +public int startCapture() { + capturing = true; + + handler.postDelayed(newFrame, 1000 / fps); + return 0; +} +``` + +The `frameProducer` thread gets a `Bitmap` representation of the `contentView` object + (the `WebView`), writes its pixels to a buffer, and then calls the `provideIntArrayFrame()` + method, passing in that buffer as a parameter: + +```java +private Runnable newFrame = new Runnable() { + @Override + public void run() { + if (capturing) { + int width = contentView.getWidth(); + int height = contentView.getHeight(); + + if (frame == null || + ScreenSharingCapturer.this.width != width || + ScreenSharingCapturer.this.height != height) { + + ScreenSharingCapturer.this.width = width; + ScreenSharingCapturer.this.height = height; + + if (bmp != null) { + bmp.recycle(); + bmp = null; + } + + bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + canvas = new Canvas(bmp); + frame = new int[width * height]; + } + canvas.saveLayer(0, 0, width, height, null); + canvas.translate(-contentView.getScrollX(), - contentView.getScrollY()); + contentView.draw(canvas); + + bmp.getPixels(frame, 0, width, 0, 0, width, height); + + provideIntArrayFrame(frame, ARGB, width, height, 0, false); + + canvas.restore(); + + handler.postDelayed(newFrame, 1000 / fps); + + } + } +}; +``` + +The `provideIntArrayFrame` method, defined by the `BaseVideoCapturer` class sends an integer array of data to the publisher, to be used for the next video frame published. + +If the publisher is still capturing video, the thread starts again after another 1/15 of a +second, so that the capturer continues to supply the publisher with new video frames to publish. +``` + +## Further Reading + +* Review [other sample projects](../) +* Read more about [OpenTok Android SDK](https://tokbox.com/developer/sdks/android/) diff --git a/Device-Screen-Sharing-Java/app/.gitignore b/Device-Screen-Sharing-Java/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Device-Screen-Sharing-Java/app/build.gradle b/Device-Screen-Sharing-Java/app/build.gradle new file mode 100644 index 00000000..bcec91f4 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' +} + +apply { + from '../../commons.gradle' +} + +android { + compileSdkVersion extCompileSdkVersion + + defaultConfig { + applicationId "com.tokbox.sample.screensharing" + minSdkVersion extMinSdkVersion + targetSdkVersion extTargetSdkVersion + versionCode extVersionCode + versionName extVersionName + } + + buildTypes { + release { + minifyEnabled extMinifyEnabled + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // Dependency versions are defined in the ../../commons.gradle file + implementation "com.opentok.android:opentok-android-sdk:${extOpentokSdkVersion}" + implementation "androidx.appcompat:appcompat:${extAppCompatVersion}" + implementation "pub.devrel:easypermissions:${extEasyPermissionsVersion}" +} diff --git a/Device-Screen-Sharing-Java/app/src/main/AndroidManifest.xml b/Device-Screen-Sharing-Java/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6b6b8eb0 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/MainActivity.java b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/MainActivity.java new file mode 100644 index 00000000..66b1e7c2 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/MainActivity.java @@ -0,0 +1,240 @@ +package com.tokbox.sample.screensharing; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.RelativeLayout; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import com.opentok.android.BaseVideoRenderer; +import com.opentok.android.OpentokError; +import com.opentok.android.Publisher; +import com.opentok.android.PublisherKit; +import com.opentok.android.Session; +import com.opentok.android.Stream; +import pub.devrel.easypermissions.AfterPermissionGranted; +import pub.devrel.easypermissions.EasyPermissions; +import java.util.List; + +public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { + + private static final String TAG = MainActivity.class.getSimpleName(); + private static final int REQUEST_MEDIA_PROJECTION = 100; + + private static final int PERMISSIONS_REQUEST_CODE = 124; + + private Session session; + private Publisher publisher; + + private RelativeLayout publisherViewContainer; + private WebView webViewContainer; + private ScreenSharingManager screenSharingManager; + private ScreenSharingCapturer screenSharingCapturer; + private MediaProjectionManager mediaProjectionManager; + private MediaProjection mediaProjection; + + private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() { + @Override + public void onStreamCreated(PublisherKit publisherKit, Stream stream) { + Log.d(TAG, "onStreamCreated: Own stream " + stream.getStreamId() + " created"); + } + + @Override + public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) { + Log.d(TAG, "onStreamDestroyed: Own stream " + stream.getStreamId() + " destroyed"); + } + + @Override + public void onError(PublisherKit publisherKit, OpentokError opentokError) { + finishWithMessage("PublisherKit error: " + opentokError.getMessage()); + } + }; + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_MEDIA_PROJECTION) { + if (resultCode != AppCompatActivity.RESULT_OK || data == null) { + Toast.makeText( + this, + "screen_capture_permission_not_granted", + Toast.LENGTH_LONG) + .show(); + return; + } + screenSharingManager.startForeground(); + startScreenCapture(resultCode, data); + } + } + + private void requestScreenCapturePermission() { + Log.d(TAG, "Requesting permission to capture screen"); + mediaProjectionManager = + (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); + + // This initiates a prompt dialog for the user to confirm screen projection. + startActivityForResult( + mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); + } + + private void startScreenCapture(int resultCode, Intent data) { + mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); + ScreenSharingCapturer screenSharingCapturer = new ScreenSharingCapturer(MainActivity.this, mediaProjection); + + publisher = new Publisher.Builder(MainActivity.this) + .capturer(screenSharingCapturer) + .build(); + + publisher.setPublisherListener(publisherListener); + publisher.setPublisherVideoType(PublisherKit.PublisherKitVideoType.PublisherKitVideoTypeScreen); + publisher.setAudioFallbackEnabled(false); + + publisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL); + publisherViewContainer.addView(publisher.getView()); + + session.publish(publisher); + } + + private Session.SessionListener sessionListener = new Session.SessionListener() { + @Override + public void onConnected(Session session) { + Log.d(TAG, "onConnected: Connected to session " + session.getSessionId()); + + if (Build.VERSION.SDK_INT >= 34) { + requestScreenCapturePermission(); + } else { + requestScreenCapturePermission(); + } + } + + @Override + public void onDisconnected(Session session) { + Log.d(TAG, "onDisconnected: disconnected from session " + session.getSessionId()); + + MainActivity.this.session = null; + } + + @Override + public void onError(Session session, OpentokError opentokError) { + finishWithMessage("Session error: " + opentokError.getMessage()); + } + + @Override + public void onStreamReceived(Session session, Stream stream) { + Log.d(TAG, "onStreamReceived: New stream " + stream.getStreamId() + " in session " + session.getSessionId()); + } + + @Override + public void onStreamDropped(Session session, Stream stream) { + Log.d(TAG, "onStreamDropped: Stream " + stream.getStreamId() + " dropped from session " + session.getSessionId()); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + screenSharingManager = new ScreenSharingManager(this); + + if(!OpenTokConfig.isValid()) { + finishWithMessage("Invalid OpenTokConfig. " + OpenTokConfig.getDescription()); + return; + } + + publisherViewContainer = findViewById(R.id.publisherview); + webViewContainer = findViewById(R.id.webview); + + requestPermissions(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (session == null) { + return; + } + + session.onPause(); + + if (isFinishing()) { + disconnectSession(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (session == null) { + return; + } + + session.onResume(); + } + + @Override + protected void onDestroy() { + disconnectSession(); + + super.onDestroy(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } + + @Override + public void onPermissionsGranted(int requestCode, List perms) { + Log.d(TAG, "onPermissionsGranted:" + requestCode + ": " + perms); + } + + @Override + public void onPermissionsDenied(int requestCode, List perms) { + finishWithMessage("onPermissionsDenied: " + requestCode + ": " + perms); + } + + @AfterPermissionGranted(PERMISSIONS_REQUEST_CODE) + private void requestPermissions() { + String[] perms = {Manifest.permission.INTERNET, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.FOREGROUND_SERVICE}; + + if (EasyPermissions.hasPermissions(this, perms)) { + session = new Session.Builder(this, OpenTokConfig.API_KEY, OpenTokConfig.SESSION_ID).build(); + session.setSessionListener(sessionListener); + session.connect(OpenTokConfig.TOKEN); + } else { + EasyPermissions.requestPermissions(this, getString(R.string.rationale_video_app), PERMISSIONS_REQUEST_CODE, perms); + } + } + + private void disconnectSession() { + if (session == null) { + return; + } + + if (publisher != null) { + publisherViewContainer.removeView(publisher.getView()); + session.unpublish(publisher); + publisher = null; + } + session.disconnect(); + } + + private void finishWithMessage(String message) { + Log.e(TAG, message); + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + this.finish(); + } +} diff --git a/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/OpenTokConfig.java b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/OpenTokConfig.java new file mode 100644 index 00000000..143e370b --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/OpenTokConfig.java @@ -0,0 +1,38 @@ +package com.tokbox.sample.screensharing; + +import android.text.TextUtils; +import androidx.annotation.NonNull; + +public class OpenTokConfig { + /* + Fill the following variables using your own Project info from the OpenTok dashboard + https://dashboard.tokbox.com/projects + */ + + // Replace with a API key + public static final String API_KEY = "47521351"; + + // Replace with a generated Session ID + public static final String SESSION_ID = "1_MX40NzUyMTM1MX5-MTcxODM1OTEyMzUyNH5oaUhWbUx4TXJQdldMY2tmRGJ1bFB3a2Z-fn4"; + + // Replace with a generated token (from the dashboard or using an OpenTok server SDK) + public static final String TOKEN = "T1==cGFydG5lcl9pZD00NzUyMTM1MSZzaWc9OWU0YTYyZWVlMDk1YjY4YmFlNjhmZDRjZjkyYjU3ODU4MjcxZjQyNzpzZXNzaW9uX2lkPTFfTVg0ME56VXlNVE0xTVg1LU1UY3hPRE0xT1RFeU16VXlOSDVvYVVoV2JVeDRUWEpRZGxkTVkydG1SR0oxYkZCM2EyWi1mbjQmY3JlYXRlX3RpbWU9MTcxODM1OTEyOCZub25jZT0wLjAxMTc0MTEzNTQ5NDAxOTExNyZyb2xlPXB1Ymxpc2hlciZleHBpcmVfdGltZT0xNzIwOTUxMTI3JmluaXRpYWxfbGF5b3V0X2NsYXNzX2xpc3Q9"; + + public static boolean isValid() { + if (TextUtils.isEmpty(OpenTokConfig.API_KEY) + || TextUtils.isEmpty(OpenTokConfig.SESSION_ID) + || TextUtils.isEmpty(OpenTokConfig.TOKEN)) { + return false; + } + + return true; + } + + @NonNull + public static String getDescription() { + return "OpenTokConfig:" + "\n" + + "API_KEY: " + OpenTokConfig.API_KEY + "\n" + + "SESSION_ID: " + OpenTokConfig.SESSION_ID + "\n" + + "TOKEN: " + OpenTokConfig.TOKEN + "\n"; + } +} diff --git a/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingCapturer.java b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingCapturer.java new file mode 100644 index 00000000..c3be0e90 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingCapturer.java @@ -0,0 +1,170 @@ +package com.tokbox.sample.screensharing; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +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.os.Handler; +import android.os.HandlerThread; +import android.util.DisplayMetrics; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.webkit.WebView; + +import java.nio.ByteBuffer; + +import com.opentok.android.BaseVideoCapturer; + +public class ScreenSharingCapturer extends BaseVideoCapturer { + + private MediaProjection mediaProjection; + private ImageReader imageReader; + private VirtualDisplay virtualDisplay; + private Handler backgroundHandler; + private HandlerThread backgroundThread; + + private Context context; + + private boolean capturing = false; + + private int fps = 15; + private int width = 0; + private int height = 0; + private int[] frame; + + public ScreenSharingCapturer(Context context, MediaProjection mediaProjection) { + this.context = context; + this.mediaProjection = mediaProjection; + initDisplayMetrics(); + } + + private void initDisplayMetrics() { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (windowManager != null) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + width = displayMetrics.widthPixels; + height = displayMetrics.heightPixels; + } + } + + @SuppressLint("WrongConstant") + @Override + public void init() { + imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2); + startBackgroundThread(); + } + + private void createVirtualDisplay() { + virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", + width, height, context.getResources().getDisplayMetrics().densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + imageReader.getSurface(), null, backgroundHandler); + + imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Image image = reader.acquireLatestImage(); + if (image != null) { + Image.Plane[] planes = image.getPlanes(); + ByteBuffer buffer = planes[0].getBuffer(); + int pixelStride = planes[0].getPixelStride(); + int rowStride = planes[0].getRowStride(); + + if (frame == null) { + frame = new int[width * height]; + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int index = y * rowStride + x * pixelStride; + int pixel = buffer.getInt(index); + frame[y * width + x] = pixel; + } + } + + provideIntArrayFrame(frame, ARGB, width, height, 0, false); + image.close(); + } + } + }, backgroundHandler); + } + + @Override + public int startCapture() { + capturing = true; + return 0; + } + + @Override + public int stopCapture() { + capturing = false; + if (virtualDisplay != null) { + virtualDisplay.release(); + } + if (mediaProjection != null) { + mediaProjection.stop(); + } + stopBackgroundThread(); + return 0; + + } + + @Override + public boolean isCaptureStarted() { + return capturing; + } + + @Override + public CaptureSettings getCaptureSettings() { + + CaptureSettings captureSettings = new CaptureSettings(); + captureSettings.fps = fps; + captureSettings.width = width; + captureSettings.height = height; + captureSettings.format = ARGB; + return captureSettings; + } + + @Override + public void destroy() { + + } + + @Override + public void onPause() { + + } + + @Override + public void onResume() { + + } + + private void startBackgroundThread() { + createVirtualDisplay(); + backgroundThread = new HandlerThread("ScreenCapture"); + backgroundThread.start(); + backgroundHandler = new Handler(backgroundThread.getLooper()); + } + + private void stopBackgroundThread() { + backgroundThread.quitSafely(); + try { + backgroundThread.join(); + backgroundThread = null; + backgroundHandler = null; + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingManager.java b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingManager.java new file mode 100644 index 00000000..efd22409 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingManager.java @@ -0,0 +1,65 @@ +package com.tokbox.sample.screensharing; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +public class ScreenSharingManager { + private ScreenSharingService mService; + private Context mContext; + private State currentState = State.UNBIND_SERVICE; + + /** Defines callbacks for service binding, passed to bindService() */ + private ServiceConnection connection = + new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + // We've bound to ScreenCapturerService, cast the IBinder and get + // ScreenCapturerService instance + ScreenSharingService.LocalBinder binder = + (ScreenSharingService.LocalBinder) service; + mService = binder.getService(); + currentState = State.BIND_SERVICE; + } + + @Override + public void onServiceDisconnected(ComponentName arg0) {} + }; + + /** An enum describing the possible states of a ScreenCapturerManager. */ + public enum State { + BIND_SERVICE, + START_FOREGROUND, + END_FOREGROUND, + UNBIND_SERVICE + } + + ScreenSharingManager(Context context) { + mContext = context; + bindService(); + } + + private void bindService() { + Intent intent = new Intent(mContext, ScreenSharingService.class); + mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + void startForeground() { + mService.startForeground(); + currentState = State.START_FOREGROUND; + } + + void endForeground() { + mService.endForeground(); + currentState = State.END_FOREGROUND; + } + + void unbindService() { + mContext.unbindService(connection); + currentState = State.UNBIND_SERVICE; + } +} diff --git a/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingService.java b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingService.java new file mode 100644 index 00000000..935ef2e4 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/java/com/tokbox/sample/screensharing/ScreenSharingService.java @@ -0,0 +1,73 @@ +package com.tokbox.sample.screensharing; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import androidx.core.app.NotificationCompat; + +@TargetApi(29) +public class ScreenSharingService extends Service { + private static final String CHANNEL_ID = "screen_capture"; + private static final String CHANNEL_NAME = "Screen_Capture"; + + // Binder given to clients + private final IBinder binder = new LocalBinder(); + + /** + * Class used for the client Binder. We know this service always runs in the same process as its + * clients, we don't need to deal with IPC. + */ + public class LocalBinder extends Binder { + public ScreenSharingService getService() { + // Return this instance of ScreenCapturerService so clients can call public methods + return ScreenSharingService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_NOT_STICKY; + } + + public void startForeground() { + NotificationChannel chan = + new NotificationChannel( + CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE); + NotificationManager manager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + assert manager != null; + manager.createNotificationChannel(chan); + + final int notificationId = (int) System.currentTimeMillis(); + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(this, CHANNEL_ID); + Notification notification = + notificationBuilder + .setOngoing(true) + .setContentTitle("ScreenCapturerService is running in the foreground") + .setPriority(NotificationManager.IMPORTANCE_MIN) + .setCategory(Notification.CATEGORY_SERVICE) + .build(); + startForeground(notificationId, notification); + } + + public void endForeground() { + stopForeground(true); + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } +} \ No newline at end of file diff --git a/Device-Screen-Sharing-Java/app/src/main/res/layout/activity_main.xml b/Device-Screen-Sharing-Java/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..17ddb4c0 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/Device-Screen-Sharing-Java/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..7dd6d427 Binary files /dev/null and b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Device-Screen-Sharing-Java/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..b1767f0a Binary files /dev/null and b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..f284f639 Binary files /dev/null and b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..f277e806 Binary files /dev/null and b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..fa8a5241 Binary files /dev/null and b/Device-Screen-Sharing-Java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Device-Screen-Sharing-Java/app/src/main/res/values-w820dp/dimens.xml b/Device-Screen-Sharing-Java/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 00000000..63fc8164 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/Device-Screen-Sharing-Java/app/src/main/res/values/colors.xml b/Device-Screen-Sharing-Java/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..3ab3e9cb --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/Device-Screen-Sharing-Java/app/src/main/res/values/dimens.xml b/Device-Screen-Sharing-Java/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..47c82246 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/Device-Screen-Sharing-Java/app/src/main/res/values/strings.xml b/Device-Screen-Sharing-Java/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..7367ce56 --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Screen-Sharing + This app needs access to your camera and mic so you can perform video calls + diff --git a/Device-Screen-Sharing-Java/app/src/main/res/values/styles.xml b/Device-Screen-Sharing-Java/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5885930d --- /dev/null +++ b/Device-Screen-Sharing-Java/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/Device-Screen-Sharing-Java/build.gradle b/Device-Screen-Sharing-Java/build.gradle new file mode 100644 index 00000000..2fcc7ef7 --- /dev/null +++ b/Device-Screen-Sharing-Java/build.gradle @@ -0,0 +1,9 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.1.2' apply false + id 'com.android.library' version '7.1.2' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/Device-Screen-Sharing-Java/gradle.properties b/Device-Screen-Sharing-Java/gradle.properties new file mode 100644 index 00000000..a48987c4 --- /dev/null +++ b/Device-Screen-Sharing-Java/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx4096m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +android.useAndroidX=true +android.enableJetifier=true +org.gradle.parallel=true \ No newline at end of file diff --git a/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.jar b/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.properties b/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..9c6be48d --- /dev/null +++ b/Device-Screen-Sharing-Java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon May 04 11:02:47 CEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip diff --git a/Device-Screen-Sharing-Java/gradlew b/Device-Screen-Sharing-Java/gradlew new file mode 100755 index 00000000..d8c4edf1 --- /dev/null +++ b/Device-Screen-Sharing-Java/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation" + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation" +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/Device-Screen-Sharing-Java/gradlew.bat b/Device-Screen-Sharing-Java/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/Device-Screen-Sharing-Java/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Device-Screen-Sharing-Java/settings.gradle b/Device-Screen-Sharing-Java/settings.gradle new file mode 100644 index 00000000..f60920c6 --- /dev/null +++ b/Device-Screen-Sharing-Java/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +include ':app'