В этом уроке разберем код приложения, которое записывает видео с экрана устройства со звуком. До Андроид 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 устанавливаем параметры виртуального экрана, передавая полученные значения.
Здесь же делаем кнопку активной и меняем текст на ней.
Запускаем приложение и наслаждаемся его работой.
при нажатии кнопки Record сбой приложения
Нужно больше информации. Какие ошибки в консоли при этом?
Ребята всем привет. Можете дать исходник у кого получилось!? Моя почта give.me.max@gmail.com
Как сделать, чтобы запись продолжалась при закрытии приложения?
Не может распознать символ stop_record и start_record
В ресурсах нужно строки с этими именами прописать
Объясните дураку, что за R и где оно объявляется
Смотрите основы на канале, урок про ресурсы https://youtu.be/RqCKvZBek90
Не могу понять:
HandlerThread serviceThread = new HandlerThread("service_thread",android.os.Process.THREAD_PRIORITY_BACKGROUND);
serviceThread.start();
Зачем? Можете объяснить?
http://www.fandroid.info/klass-thread-i-interfejs-runnable-zhiznennyj-tsikl-potoka-java/
https://habrahabr.ru/post/164487/
https://developer.android.com/guide/components/processes-and-threads.html?hl=ru#Threads