Commit b6091437 authored by Luca Stefani's avatar Luca Stefani Committed by Joey

Recorder: Use Media projection APIs

Change-Id: If2039a5a96b5b84dc1856af746a7a781ab5f23ff
parent b5679f94
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 29
defaultConfig {
applicationId "org.lineageos.recorder"
minSdkVersion 24
targetSdkVersion 27
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName "1.1"
}
......@@ -25,8 +24,8 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'com.google.android.material:material:1.1.0-alpha02'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0-alpha10'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
}
......@@ -17,18 +17,14 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.lineageos.recorder">
<uses-permission android:name="android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!-- This is needed for aosp build env -->
<uses-sdk android:minSdkVersion="24"
tools:ignore="GradleOverrides" />
<uses-sdk tools:ignore="GradleOverrides" />
<application
android:allowBackup="false"
......@@ -62,10 +58,14 @@
<service android:name=".sounds.SoundRecorderService" />
<!-- Screen recorder -->
<service android:name=".screen.ScreencastService" />
<service
android:name=".screen.ScreencastService"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />
<!-- Screen recorder overlay -->
<service android:name=".screen.OverlayService" />
<service
android:name=".screen.OverlayService" />
<receiver
android:name=".screen.ScreencastStartReceiver"
......
......@@ -16,6 +16,7 @@
package org.lineageos.recorder;
import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
......@@ -24,26 +25,29 @@ import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.transition.TransitionManager;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.transition.TransitionManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.lineageos.recorder.screen.OverlayService;
import org.lineageos.recorder.screen.ScreencastService;
......@@ -61,6 +65,7 @@ public class RecorderActivity extends AppCompatActivity implements
private static final int REQUEST_SCREEN_REC_PERMS = 439;
private static final int REQUEST_SOUND_REC_PERMS = 440;
private static final int REQUEST_DIALOG_ACTIVITY = 441;
private static final int REQUEST_AUDIO_VIDEO = 442;
private static final int[] PERMISSION_ERROR_MESSAGE_RES_IDS = {
0,
......@@ -272,12 +277,23 @@ public class RecorderActivity extends AppCompatActivity implements
.setClass(this, ScreencastService.class));
} else {
// Start
new Handler().postDelayed(() -> {
Intent intent = new Intent(this, OverlayService.class);
intent.putExtra(OverlayService.EXTRA_HAS_AUDIO, isAudioAllowedWithScreen());
startService(intent);
onBackPressed();
}, 500);
MediaProjectionManager mediaProjectionManager = getSystemService(
MediaProjectionManager.class);
Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(permissionIntent, REQUEST_AUDIO_VIDEO);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_AUDIO_VIDEO && resultCode == Activity.RESULT_OK) {
Intent intent = new Intent(this, OverlayService.class);
intent.putExtra(OverlayService.EXTRA_HAS_AUDIO, isAudioAllowedWithScreen());
intent.putExtra(OverlayService.EXTRA_RESULT_CODE, resultCode);
intent.putExtra(OverlayService.EXTRA_RESULT_DATA, data);
startService(intent);
finish();
}
}
......@@ -359,7 +375,7 @@ public class RecorderActivity extends AppCompatActivity implements
return false;
}
String[] permissionArray = permissions.toArray(new String[permissions.size()]);
String[] permissionArray = permissions.toArray(new String[0]);
requestPermissions(permissionArray, REQUEST_SOUND_REC_PERMS);
return true;
}
......@@ -377,7 +393,7 @@ public class RecorderActivity extends AppCompatActivity implements
return true;
}
return true;
return false;
}
private boolean isAudioAllowedWithScreen() {
......
/*
* Copyright (C) 2013 The CyanogenMod Project
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.recorder.screen;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.EncoderCapabilities;
import android.media.EncoderCapabilities.VideoEncoderCap;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.util.Log;
import android.view.Surface;
import org.lineageos.recorder.R;
import java.io.IOException;
import java.util.List;
abstract class EncoderDevice {
final Context context;
private final String LOGTAG = getClass().getSimpleName();
private static final List<VideoEncoderCap> videoEncoders =
EncoderCapabilities.getVideoEncoders();
// Standard resolution tables, removed values that aren't multiples of 8
private final int[][] validResolutions = {
// CEA Resolutions
{640, 480},
{720, 480},
{720, 576},
{1280, 720},
{1920, 1080},
// VESA Resolutions
{800, 600},
{1024, 768},
{1152, 864},
{1280, 768},
{1280, 800},
{1360, 768},
{1366, 768},
{1280, 1024},
//{ 1400, 1050 },
//{ 1440, 900 },
//{ 1600, 900 },
{1600, 1200},
//{ 1680, 1024 },
//{ 1680, 1050 },
{1920, 1200},
// HH Resolutions
{800, 480},
{854, 480},
{864, 480},
{640, 360},
//{ 960, 540 },
{848, 480}
};
private MediaCodec venc;
private int width;
private int height;
private VirtualDisplay virtualDisplay;
EncoderDevice(Context context, int width, int height) {
this.context = context;
this.width = width;
this.height = height;
}
VirtualDisplay registerVirtualDisplay(Context context) {
assert virtualDisplay == null;
DisplayManager dm = context.getSystemService(DisplayManager.class);
Surface surface = createDisplaySurface();
if (surface == null || dm == null)
return null;
return virtualDisplay = dm.createVirtualDisplay(ScreencastService.SCREENCASTER_NAME,
width, height, 1,
surface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE);
}
void stop() {
if (venc != null) {
try {
venc.signalEndOfInputStream();
} catch (Exception ignored) {
}
venc = null;
}
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
}
private void destroyDisplaySurface(MediaCodec venc) {
if (venc == null) {
return;
}
// release this surface
try {
venc.stop();
venc.release();
} catch (Exception ignored) {
}
// see if this device is still in use
if (this.venc != venc) {
return;
}
// display is done, kill it
this.venc = null;
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
}
protected abstract EncoderRunnable onSurfaceCreated(MediaCodec venc);
private Surface createDisplaySurface() {
if (venc != null) {
// signal any old crap to end
try {
venc.signalEndOfInputStream();
} catch (Exception ignored) {
}
venc = null;
}
int maxWidth = 640;
int maxHeight = 480;
int bitrate = 2000000;
for (VideoEncoderCap cap : videoEncoders) {
if (cap.mCodec == MediaRecorder.VideoEncoder.H264) {
maxWidth = cap.mMaxFrameWidth;
maxHeight = cap.mMaxFrameHeight;
bitrate = cap.mMaxBitRate;
}
}
int max = Math.max(maxWidth, maxHeight);
int min = Math.min(maxWidth, maxHeight);
int resConstraint = context.getResources().getInteger(
R.integer.config_maxDimension);
double ratio;
boolean landscape = false;
boolean resizeNeeded = false;
// see if we need to resize
// Figure orientation and ratio first
if (width > height) {
// landscape
landscape = true;
ratio = (double) width / (double) height;
if (resConstraint >= 0 && height > resConstraint) {
min = resConstraint;
}
if (width > max || height > min) {
resizeNeeded = true;
}
} else {
// portrait
ratio = (double) height / (double) width;
if (resConstraint >= 0 && width > resConstraint) {
min = resConstraint;
}
if (height > max || width > min) {
resizeNeeded = true;
}
}
if (resizeNeeded) {
boolean matched = false;
for (int[] resolution : validResolutions) {
// All res are in landscape. Find the highest match
if (resolution[0] <= max && resolution[1] <= min &&
(!matched || (resolution[0] > (landscape ? width : height)))) {
if (((double) resolution[0] / (double) resolution[1]) == ratio) {
// Got a valid one
if (landscape) {
width = resolution[0];
height = resolution[1];
} else {
height = resolution[0];
width = resolution[1];
}
matched = true;
}
}
}
if (!matched) {
// No match found. Go for the lowest... :(
width = landscape ? 640 : 480;
height = landscape ? 480 : 640;
}
}
MediaFormat video = MediaFormat.createVideoFormat("video/avc", width, height);
video.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
video.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
video.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
video.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3);
// create a surface from the encoder
Log.i(LOGTAG, "Starting encoder at " + width + "x" + height);
try {
venc = MediaCodec.createEncoderByType("video/avc");
} catch (IOException e) {
Log.wtf(LOGTAG, "Can't create AVC encoder!", e);
}
venc.configure(video, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = venc.createInputSurface();
venc.start();
EncoderRunnable runnable = onSurfaceCreated(venc);
new Thread(runnable, "Encoder").start();
return surface;
}
abstract class EncoderRunnable implements Runnable {
MediaCodec venc;
EncoderRunnable(MediaCodec venc) {
this.venc = venc;
}
abstract void encode() throws Exception;
void cleanup() {
destroyDisplaySurface(venc);
venc = null;
}
@Override
final public void run() {
try {
encode();
} catch (Exception e) {
Log.e(LOGTAG, "EncoderDevice error", e);
} finally {
cleanup();
Log.i(LOGTAG, "=======ENCODING COMPLETE=======");
}
}
}
}
......@@ -15,14 +15,15 @@
*/
package org.lineageos.recorder.screen;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import org.lineageos.recorder.R;
......@@ -36,6 +37,8 @@ public class OverlayService extends Service {
"screencast_overlay_notification_channel";
public static final String EXTRA_HAS_AUDIO = "extra_audio";
public static final String EXTRA_RESULT_CODE = "extra_result_code";
public static final String EXTRA_RESULT_DATA = "extra_result_data";
private final static int FG_ID = 123;
/* Horrible hack to determine whether the service is running:
......@@ -48,13 +51,16 @@ public class OverlayService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int id) {
boolean hasAudio = intent != null && intent.getBooleanExtra(EXTRA_HAS_AUDIO, false);
if (intent == null) {
return START_NOT_STICKY;
}
boolean hasAudio = intent.getBooleanExtra(EXTRA_HAS_AUDIO, false);
int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED);
Intent data = intent.getParcelableExtra(EXTRA_RESULT_DATA);
mLayer = new OverlayLayer(this);
mLayer.setOnActionClickListener(() -> {
Intent fabIntent = new Intent(ScreencastService.ACTION_START_SCREENCAST);
fabIntent.putExtra(ScreencastService.EXTRA_WITHAUDIO, hasAudio);
startService(fabIntent.setClass(this, ScreencastService.class));
startService(ScreencastService.getStartIntent(this, resultCode, data, hasAudio));
Utils.setStatus(getApplication(), Utils.UiStatus.SCREEN);
onDestroy();
});
......@@ -84,8 +90,7 @@ public class OverlayService extends Service {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
notificationManager == null || notificationManager
if (notificationManager == null || notificationManager
.getNotificationChannel(SCREENCAST_OVERLAY_NOTIFICATION_CHANNEL) != null) {
return;
}
......
......@@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
......
#Mon Jan 07 12:15:02 CET 2019
#Fri Sep 27 17:19:32 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment