Как создать приложение для записи экрана андроид-смартфона

В этом уроке разберем код приложения, которое записывает видео с экрана устройства со звуком. До Андроид 5.0 приложение для записи видео с экрана мобильных устройств требовало рут-доступ и не гарантировало нормальную работу на устройствах разных производителей. Все изменилось в API 21 версии. Здесь появился класс MediaProjection, который предоставляет доступ для записи видео с экрана или звука с аудио системы.
Давайте рассмотрим код приложения для записи видео с экрана устройства.

Макет экрана содержит одну кнопку для старта и остановки записи.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
    android:id="@+id/start_record"
    android:layout_centerInParent="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/start_record"/>
</RelativeLayout>

Перейдем к коду. В главном пакете видим три класса- класс RecordApplication, класс MainActivity и класс RecordService.

Рассмотрим класс RecordApplication. Его вызов происходит в манифесте в секции application в строке android:name=.

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.glgjing.recorder"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

    <application
        android:name="com.glgjing.recorder.RecordApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name="com.glgjing.recorder.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service android:name="com.glgjing.recorder.RecordService"/>

    </application>

</manifest>

Класс RecordApplication унаследован от класса Application. Согласно документации, класс Application или его наследник инициализируется перед любым другим классом, когда создается процесс приложения.

package com.glgjing.recorder;

import android.app.Application;
import android.content.Context;
import android.content.Intent;


public class RecordApplication extends Application {

  private static RecordApplication application;

  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    application = this;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    // Start service
    startService(new Intent(this, RecordService.class));
  }

  public static RecordApplication getInstance() {
    return application;
  }
}

Здесь вызывается метод startService, который запускает сервис RecordService при старте приложения.

Класс RecordService унаследован от класса Service. О том, что такое сервисы, подробно можно узнать из видеоуроков на нашем канале, начиная с урока 92.
Если в двух словах, сервис – это некая задача, которая работает в фоне и не использует UI. Поскольку нам нужно записывать все, что происходит на экране, независимо от того, какое приложение запущено, поэтому мы и будем использовать сервис.

package com.glgjing.recorder;

import android.app.Service;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.os.Binder;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.IBinder;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;


public class RecordService extends Service {
  private MediaProjection mediaProjection;
  private MediaRecorder mediaRecorder;
  private VirtualDisplay virtualDisplay;

  private boolean running;
  private int width = 720;
  private int height = 1080;
  private int dpi;


  @Override
  public IBinder onBind(Intent intent) {
    return new RecordBinder();
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    return START_STICKY;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    HandlerThread serviceThread = new HandlerThread("service_thread",
        android.os.Process.THREAD_PRIORITY_BACKGROUND);
    serviceThread.start();
    running = false;
    mediaRecorder = new MediaRecorder();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
  }

  public void setMediaProject(MediaProjection project) {
    mediaProjection = project;
  }

  public boolean isRunning() {
    return running;
  }

  public void setConfig(int width, int height, int dpi) {
    this.width = width;
    this.height = height;
    this.dpi = dpi;
  }

  public boolean startRecord() {
    if (mediaProjection == null || running) {
      return false;
    }

    initRecorder();
    createVirtualDisplay();
    mediaRecorder.start();
    running = true;
    return true;
  }

  public boolean stopRecord() {
    if (!running) {
      return false;
    }
    running = false;
    mediaRecorder.stop();
    mediaRecorder.reset();
    virtualDisplay.release();
    mediaProjection.stop();

    return true;
  }

  private void createVirtualDisplay() {
    virtualDisplay = mediaProjection.createVirtualDisplay("MainScreen", width, height, dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.getSurface(), null, null);
  }

  private void initRecorder() {
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
    mediaRecorder.setOutputFile(getsaveDirectory() + System.currentTimeMillis() + ".mp4");
    mediaRecorder.setVideoSize(width, height);
    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
    mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024);
    mediaRecorder.setVideoFrameRate(30);
    try {
      mediaRecorder.prepare();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public String getsaveDirectory() {
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
      String rootDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "ScreenRecord" + "/";

      File file = new File(rootDir);
      if (!file.exists()) {
        if (!file.mkdirs()) {
          return null;
        }
      }

      Toast.makeText(getApplicationContext(), rootDir, Toast.LENGTH_SHORT).show();

      return rootDir;
    } else {
      return null;
    }
  }

  public class RecordBinder extends Binder {
    public RecordService getRecordService() {
      return RecordService.this;
    }
  }
}

Здесь объявлены переменные классов:

MediaProjection – это токен, предоставляющий приложению возможность захватить содержимое экрана и/или записывать аудио системы.
MediaRecorder – класс, который используется для записи аудио и видео.
VirtualDisplay Представляет собой виртуальный экран, содержимое которого рендерится в Surface, который мы передаем методу createVirtualDisplay.

О том, что такое Surface, мы говорим в уроке 132.
В двух словах – это компонент, на который выводится изображение.
Объявляем еще несколько переменных: логическую running, которой будем присваивать true в процессе записи.

Далее параметры виртуального экрана, разрешение установим, а плотность пока не указываем.

Далее идет метод IBinder onBind, который позволяет приложению подключиться к сервису и взаимодействовать с ним через возвращаемый объект RecordBinder. Подробнее в уроке 97

Теперь методы жизненного цикла сервиса.

Метод onStartCommand срабатывает при старте сервиса методом startService, который вызывается в классе RecordApplication. Он возвращает флаг START_STICKY – это значит, сервис будет перезапущен, если будет убит системой.

В методе onCreate? который вызывается в начале работы сервиса, создаем отдельный поток serviceThread с использованием класса HandlerThread. Это вспомогательный класс для запуска нового потока, который имеет лупер, который может использоваться для создания обработчиков классов. На вход передаем произвольное имя потока и флаг THREAD_PRIORITY_BACKGROUND – Стандартный приоритет фоновых потоков.

Далее стартуем поток, сбрасываем значение переменной running и создаем mediaRecorder.

В методе onDestroy, который вызывается при остановке сервиса, просто вызываем метод суперкласса.

Метод setMediaProject будет вызываться в MainActivity и передавать объект mediaProjection.

Далее геттер для переменной running.

Метод setConfig устанавливает параметры виртуального экрана.

В методе startRecord проверяем, если объект mediaProjection не существует и переменная running имеет значение true, возвращаем false.
Вызываем здесь методы initRecorder и createVirtualDisplay, которые рассмотрим позже, стартуем запись вызовом mediaRecorder.start(); присваиваем переменной running = true; и возвращаем true.

Метод stopRecord выполняет обратные операции, останавливает запись и перезапускает mediaRecorder в состояние ожидания. Освобождаем virtualDisplay и останавливаем mediaProjection.

Теперь метод createVirtualDisplay, который вызывается выше в методе startRecord. Здесь выполняется создание виртуального экрана через метод mediaProjection.createVirtualDisplay, которому передается произвольное имя дисплея, его параметры, флаг VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, который позволяет отразить содержимое приватного дисплея, если его содержимое не отображается, и Surface.

Теперь в методе initRecorder, который вызывается выше в методе startRecord работаем с объектом mediaRecorder.
Метод setAudioSource устанавливает источник звука, используемый для записи.
setVideoSource задает источник видео, который будет использоваться для записи.
setOutputFormat устанавливает формат получаемого файла записи.
setOutputFile устанавливает целевое местоположение и имя файла записи.
setVideoSize устанавливает размер видео.
setVideoEncoder определяет кодировщик видео.
setAudioEncoder определяет кодировщик аудио.
setVideoEncodingBitRate устанавливает битрейт файла записи, здесь жестко прописано постоянное значение, равное 5 Мбит.
setVideoFrameRate задает частоту кадров, здесь 30 кадров в секунду.
Метод prepare() подготавливает mediaRecorder для записи и кодирования данных. Выполняем его в блоке try…catch с перехватом ошибки IOException.

Далее в методе getsaveDirectory задаем путь для сохранения файла записи и показываем тост об этом пользователю.

Ниже здесь создаем внутренний класс RecordBinder, это биндер для связи и взаимодействия с сервисом в приложении.

В классе MainActivity.java объявлены константы _REQUEST_CODE с произвольными значениями. Эти константы используются в интентах и запросах разрешений, чтобы отличать друг от друга пришедшие результаты. Подробнее об этом смотрите урок 30

package com.glgjing.recorder;

import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.IBinder;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;


public class MainActivity extends AppCompatActivity {
  private static final int RECORD_REQUEST_CODE  = 101;
  private static final int STORAGE_REQUEST_CODE = 102;
  private static final int AUDIO_REQUEST_CODE   = 103;

  private MediaProjectionManager projectionManager;
  private MediaProjection mediaProjection;
  private RecordService recordService;
  private Button startBtn;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
    setContentView(R.layout.activity_main);

    startBtn = (Button) findViewById(R.id.start_record);
    startBtn.setEnabled(false);
    startBtn.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        if (recordService.isRunning()) {
          recordService.stopRecord();
          startBtn.setText(R.string.start_record);
        } else {
          Intent captureIntent = projectionManager.createScreenCaptureIntent();
          startActivityForResult(captureIntent, RECORD_REQUEST_CODE);
        }
      }
    });

    if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(this,
          new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_REQUEST_CODE);
    }

    if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
        != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(this,
          new String[] {Manifest.permission.RECORD_AUDIO}, AUDIO_REQUEST_CODE);
    }

    Intent intent = new Intent(this, RecordService.class);
    bindService(intent, connection, BIND_AUTO_CREATE);
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    unbindService(connection);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == RECORD_REQUEST_CODE && resultCode == RESULT_OK) {
      mediaProjection = projectionManager.getMediaProjection(resultCode, data);
      recordService.setMediaProject(mediaProjection);
      recordService.startRecord();
      startBtn.setText(R.string.stop_record);
    }
  }

  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == STORAGE_REQUEST_CODE || requestCode == AUDIO_REQUEST_CODE) {
      if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
        finish();
      }
    }
  }

  private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
      DisplayMetrics metrics = new DisplayMetrics();
      getWindowManager().getDefaultDisplay().getMetrics(metrics);
      RecordService.RecordBinder binder = (RecordService.RecordBinder) service;
      recordService = binder.getRecordService();
      recordService.setConfig(metrics.widthPixels, metrics.heightPixels, metrics.densityDpi);
      startBtn.setEnabled(true);
      startBtn.setText(recordService.isRunning() ? R.string.stop_record : R.string.start_record);
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {}
  };
}

Далее объявляем переменные классов MediaProjectionManager – Управляет получением определенных типов токенов MediaProjection.

MediaProjection вы уже знаете – это токен, предоставляющий приложению возможность захватить содержимое экрана и/или записывать аудио системы.

Также объявляем экземпляр нашего сервиса RecordService и обычную кнопку.

В методе onCreate получаем экземпляр MediaProjectionManager для управления сессиями отображения медиа-данных.

Создаем кнопку, по умолчанию неактивную, и слушатель для нее.
В методе onClick по нажатию кнопки будем вызывать метод recordService.stopRecord, в случае,если запись идет. Иначе создаем интент с projectionManager.createScreenCaptureIntent() и отправляем его методом startActivityForResult.

Далее идут запросы разрешений на запись в память устройства и на запись аудио.

Еще здесь мы создаем еще один интент для запуска сервиса и передаем его методу bindService, который выполняет привязку сервиса к приложению, а если сервис не работает, то стартует его предварительно.

В методе onDestroy отвязываем сервис.

Метод onActivityResult получает результат вызова метода startActivityForResult, где мы отправляем captureIntent и RECORD_REQUEST_CODE.
Если запрос прошел успешно, создадим объект MediaProjection, полученный от успешного запроса захвата экрана. Он будет иметь значение NULL, если результат от startActivityForResult() будет не RESULT_OK.

Вызываем метод recordService.startRecord(); и меняем текст кнопки на “Остановить запись”

Метод onRequestPermissionsResult – это обратный вызов для результата запроса разрешений. Этот метод вызывается для каждого вызова метода requestPermissions. Вполне возможно, что процесс запроса разрешений с пользователем был прерван. В этом случае вы получите пустые разрешения и массивы, которые должны рассматриваться как отмена.

Далее создается экземпляр интерфейса ServiceConnection с реализацией его методов onServiceConnected и onServiceDisconnected. Интерфейс служит для мониторинга состояния сервиса.
Второй метод оставляем пустым, а в первом методе onServiceConnected, который вызывается в случае подключения приложения к сервису через биндер, создаем объект класса DisplayMetrics, который служит для определения реальных параметров экрана устройства – разрешения и плотности.

Получаем экземпляр recordService и через его метод setConfig устанавливаем параметры виртуального экрана, передавая полученные значения.
Здесь же делаем кнопку активной и меняем текст на ней.

Запускаем приложение и наслаждаемся его работой.

Коментарі: 10
  1. zelgorod@gmail.com
    zelgorod@gmail.com

    при нажатии кнопки Record сбой приложения

    1. Виталий Непочатов
      admin (автор)

      Нужно больше информации. Какие ошибки в консоли при этом?

  2. luckymaxyk
    luckymaxyk

    Ребята всем привет. Можете дать исходник у кого получилось!? Моя почта give.me.max@gmail.com

  3. Как сделать, чтобы запись продолжалась при закрытии приложения?

  4. KirkTem
    KirkTem

    Не может распознать символ stop_record и start_record

    1. Hawoline
      Hawoline

      В ресурсах нужно строки с этими именами прописать

  5. KirkTem
    KirkTem

    Объясните дураку, что за R и где оно объявляется

    1. Виталий Непочатов
      Виталий Непочатов

      Смотрите основы на канале, урок про ресурсы https://youtu.be/RqCKvZBek90

  6. Алексей

    Не могу понять:
    HandlerThread serviceThread = new HandlerThread("service_thread",
    android.os.Process.THREAD_PRIORITY_BACKGROUND);
    serviceThread.start();

    Зачем? Можете объяснить?

Додати коментар