В этом уроке разберем код приложения, которое записывает видео с экрана устройства со звуком. До Андроид 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