Продолжаем разбираться с CAMERA2 API Android.
В предыдущей статье мы осваивали работу камеры, чтобы делать фоточки, используя новое API. Теперь же займёмся съемкой видео. Вообще изначально, главной моей целью был стрим по сети живого видео с камеры Android при помощи Media Codec, но так уж вышло, что сначала на сцену вылез Media Recorder и захотел поделиться с почтеннейшей публикой тем, как хорошо он умеет записывать видосики. Поэтому стримингом мы займёмся в следующий раз, а пока разберёмся, как присобачить Media Recorder к новому API. Пост про него получился довольно банальным, поэтому под кат могут заглядывать только новички и совершеннейшие чайники.
Итак, Media Recorder
Как мы видим из самого названия класса и приведённой выше картинки, Media Recorder нужен нам для того, чтобы взять где-то источник аудио или видео или всё вместе и записать в итоге, всё это в файл в желаемом, а главное доступном формате.
В нашем случае задача простая, берем видео и аудио с камеры и микрофона и пишем в файл в формате MPEG_4. Некоторые извращенцы бывало подсовывали для Media Recorder вместо файла сетевой сокет, чтобы иметь возможность гнать видос по сети, но к счастью, эти пещерные времена уже в прошлом. Мы займемся подобным в следующей статье, но возьмём для этого уже цивилизованный Media Codec.
Как все помнят по предыдущему Camera API из далекого 2011, тогда подключение MediaRecorder не составляло никакой сложности. Приятно отметить, никакой сложности не возникает и теперь. И пусть нас не пугает картинка полной схемы работы камеры.
Нам всего лишь нужно пристегнуть Media Recorder к поверхности Surface на которую выводится изображение с камеры, а дальше он всё сделает сам. С аудио ещё тривиальнее, просто задаем нужные форматы, и Media Recorder разберется со звуком самостоятельно, не докучая нам всякими коллбэками.
Помните, как удивлялся японский товарищ из прошлого поста:
Одна из причин почему Camera2 приводит в недоумение, это то насколько много коллбэков надо использовать, чтобы сделать один снимок.
А здесь, наоборот, удивительно то, насколько мало коллбэков нужно чтобы записать видео файл. Всего два.
И сейчас мы их напишем
Как исходное, возьмём код из прошлой статьи и выкинем из него всё, что относится к фотографированию и оставим, по сути, лишь разрешения и инициализацию камеры. Камеру тоже оставим всего одну — фронтальную.
private CameraManager mCameraManager = null;
private final int CAMERA1 = 0;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(LOG_TAG, "Запрашиваем разрешение");
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
||
(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
||
(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED)
) {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);
}
mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
// Получение списка камер с устройства
myCameras = new CameraService[mCameraManager.getCameraIdList().length];
for (String cameraID : mCameraManager.getCameraIdList()) {
Log.i(LOG_TAG, "cameraID: " + cameraID);
int id = Integer.parseInt(cameraID);
// создаем обработчик для камеры
myCameras[id] = new CameraService(mCameraManager, cameraID);
}
} catch (CameraAccessException e) {
Log.e(LOG_TAG, e.getMessage());
e.printStackTrace();
}
public class CameraService {
private String mCameraID;
private CameraDevice mCameraDevice = null;
private CameraCaptureSession mSession;
private CaptureRequest.Builder mPreviewBuilder;
public CameraService(CameraManager cameraManager, String cameraID) {
mCameraManager = cameraManager;
mCameraID = cameraID;
}
private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
Log.i(LOG_TAG, "Open camera with id:" + mCameraDevice.getId());
//startCameraPreviewSession(); здесь запустим камеру и пристегнём к ней Media Recorder
}
@Override
public void onDisconnected(CameraDevice camera) {
mCameraDevice.close();
Log.i(LOG_TAG, "disconnect camera with id:" + mCameraDevice.getId());
mCameraDevice = null;
}
@Override
public void onError(CameraDevice camera, int error) {
Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);
}
};
Как мы видим, в разрешениях прибавилась опция RECORD_AUDIO. Без него Media Recorder сможет записать только голое видео без звука. А если мы попытаемся все-таки указать звуковые форматы без разрешения, то он не запустится вообще. Поэтому разрешаем запись звука и прочее, помня, конечно, о том что в реальном коде в главном потоке такие вещи делать нехорошо, а хорошо только в демонстрационном.
Далее инициализируем сам Media Recorder в отдельном методе
private void setUpMediaRecorder() {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mCurrentFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test"+count+".mp4");
mMediaRecorder.setOutputFile(mCurrentFile.getAbsolutePath());
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
try {
mMediaRecorder.prepare();
Log.i(LOG_TAG, " запустили медиа рекордер");
} catch (Exception e) {
Log.i(LOG_TAG, "не запустили медиа рекордер");
}
}
Тут тоже всё ясно-понятно и пояснений давать не требуется.
Далее наступает самый ответственный этап — присобачивание Media Recorder к Surface. В прошлом посте мы выводили на Surface изображение с камеры и с него же снимали кадр при помощи Image Reader. Для этого мы просто указывали оба компонента в списке Surface.
Arrays.asList(surface,mImageReader.getSurface())
Здесь то же самое, только вместо ImageReader указываем:
(Arrays.asList(surface, mMediaRecorder.getSurface()).
Там вообще, через запятую, можно что угодно лепить, все используемые вами компоненты и даже Media Codec. То есть, вы можете в одном окне делать фотки, снимать видео и стримить его. Surface добрый — позволяет. Правда, можно ли всё делать одновременно, этого не подскажу. По идее, судя по картинке работы камеры — можно.
Должно, вроде как, просто разлетаться по разным потокам. Так что поле для экспериментов есть.
Но вернёмся к Media Recorder
Практически мы сделали всё. Нам не нужно в отличие от фотографирования никаких дополнительных реквестов для съёмки, не нужен никакой аналог ImageSaver – наш работяга рекордер делает всё сам. И это приятно.
В итоге программа приобретает совершенно минималистический вид.
package com.example.mediarecorder1;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.media.CamcorderProfile;
import android.os.Bundle;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import java.io.File;
import java.util.Arrays;
public class MainActivity extends AppCompatActivity {
public static final String LOG_TAG = "myLogs";
CameraService[] myCameras = null;
private CameraManager mCameraManager = null;
private final int CAMERA1 = 0;
private int count =1;
private Button mButtonOpenCamera1 = null;
private Button mButtonRecordVideo = null;
private Button mButtonStopRecordVideo = null;
public static TextureView mImageView = null;
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler = null;
private File mCurrentFile;
private MediaRecorder mMediaRecorder = null;
private void startBackgroundThread() {
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
private void stopBackgroundThread() {
mBackgroundThread.quitSafely();
try {
mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(LOG_TAG, "Запрашиваем разрешение");
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
||
(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
||
(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED)
) {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);
}
mButtonOpenCamera1 = findViewById(R.id.button1);
mButtonRecordVideo = findViewById(R.id.button2);
mButtonStopRecordVideo = findViewById(R.id.button3);
mImageView = findViewById(R.id.textureView);
mButtonOpenCamera1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (myCameras[CAMERA1] != null) {
if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();
}
}
});
mButtonRecordVideo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ((myCameras[CAMERA1] != null) & mMediaRecorder != null) {
mMediaRecorder.start();
}
}
});
mButtonStopRecordVideo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ((myCameras[CAMERA1] != null) & (mMediaRecorder != null)) {
myCameras[CAMERA1].stopRecordingVideo();
}
}
});
mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
// Получение списка камер с устройства
myCameras = new CameraService[mCameraManager.getCameraIdList().length];
for (String cameraID : mCameraManager.getCameraIdList()) {
Log.i(LOG_TAG, "cameraID: " + cameraID);
int id = Integer.parseInt(cameraID);
// создаем обработчик для камеры
myCameras[id] = new CameraService(mCameraManager, cameraID);
}
} catch (CameraAccessException e) {
Log.e(LOG_TAG, e.getMessage());
e.printStackTrace();
}
setUpMediaRecorder();
}
private void setUpMediaRecorder() {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mCurrentFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test"+count+".mp4");
mMediaRecorder.setOutputFile(mCurrentFile.getAbsolutePath());
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
try {
mMediaRecorder.prepare();
Log.i(LOG_TAG, " запустили медиа рекордер");
} catch (Exception e) {
Log.i(LOG_TAG, "не запустили медиа рекордер");
}
}
public class CameraService {
private String mCameraID;
private CameraDevice mCameraDevice = null;
private CameraCaptureSession mSession;
private CaptureRequest.Builder mPreviewBuilder;
public CameraService(CameraManager cameraManager, String cameraID) {
mCameraManager = cameraManager;
mCameraID = cameraID;
}
private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
Log.i(LOG_TAG, "Open camera with id:" + mCameraDevice.getId());
startCameraPreviewSession();
}
@Override
public void onDisconnected(CameraDevice camera) {
mCameraDevice.close();
Log.i(LOG_TAG, "disconnect camera with id:" + mCameraDevice.getId());
mCameraDevice = null;
}
@Override
public void onError(CameraDevice camera, int error) {
Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);
}
};
private void startCameraPreviewSession() {
SurfaceTexture texture = mImageView.getSurfaceTexture();
texture.setDefaultBufferSize(640, 480);
Surface surface = new Surface(texture);
try {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
/**Surface for the camera preview set up*/
mPreviewBuilder.addTarget(surface);
/**MediaRecorder setup for surface*/
Surface recorderSurface = mMediaRecorder.getSurface();
mPreviewBuilder.addTarget(recorderSurface);
mCameraDevice.createCaptureSession(Arrays.asList(surface, mMediaRecorder.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
mSession = session;
try {
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public void stopRecordingVideo() {
try {
mSession.stopRepeating();
mSession.abortCaptures();
mSession.close();
} catch (CameraAccessException e) {
e.printStackTrace();
}
mMediaRecorder.stop();
mMediaRecorder.release();
count++;
setUpMediaRecorder();
startCameraPreviewSession();
}
public boolean isOpen() {
if (mCameraDevice == null) {
return false;
} else {
return true;
}
}
public void openCamera() {
try {
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);
}
} catch (CameraAccessException e) {
Log.i(LOG_TAG, e.getMessage());
}
}
}
@Override
public void onPause() {
stopBackgroundThread();
super.onPause();
}
@Override
public void onResume() {
super.onResume();
startBackgroundThread();
}
}
добавляем к ней LAYOUT
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextureView
android:id="@+id/textureView"
android:layout_width="356dp"
android:layout_height="410dp"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.49"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="292dp"
android:layout_height="145dp"
android:layout_marginStart="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textureView"
app:layout_constraintVertical_bias="0.537">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="КАМЕРА ПЕРЕДНЯЯ" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="НАЧАТЬ ЗАПИСЬ" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ЗАКОНЧИТЬ ЗАПИСЬ" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
И небольшое дополнение в манифест
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Всё работает и успешно пишет файлы.
Единственное, защиты от дурака нет и поэтому, если неразумно тыкать в экранные кнопки в рандомном порядке, то можно всё сломать.
Javian
При блокировке экрана как себя поведет?
Norsat
del
Tarson Автор
Да особо и не тестировал. Собственно, это для демонстрации работы — tutorial. Тут, чем проще тем лучше.