Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

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

Recorder: Use Media projection APIs

Change-Id: If2039a5a96b5b84dc1856af746a7a781ab5f23ff
parent b5679f94
Loading
Loading
Loading
Loading
+6 −7
Original line number Diff line number Diff line
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'
}
+8 −8
Original line number Diff line number Diff line
@@ -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.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"
+33 −17
Original line number Diff line number Diff line
@@ -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(() -> {
            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);
                onBackPressed();
            }, 500);
            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() {
+0 −270
Original line number Diff line number Diff line
/*
 * 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=======");
            }
        }
    }
}
+12 −7
Original line number Diff line number Diff line
@@ -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;
        }
Loading