Android Camera2 API от чайника, часть 2, пишем видео

  • Tutorial


Продолжаем разбираться с 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"/>



Всё работает и успешно пишет файлы.
Единственное, защиты от дурака нет и поэтому, если неразумно тыкать в экранные кнопки в рандомном порядке, то можно всё сломать.

P.S.

После опубликования поста пытливым читателем было замечено существование заметного лага изображения с камеры после нажатия кнопки, «закончить запись», перед тем, как вы пожелаете сделать запись новую. Тогда как, попробовав сделать то же самое на стандартной камере Android, вы никаких лагов не увидите, все будет вполне себе плавненько.

Как оказалось, задержка происходит по двум причинам.
Первая — это пересоздание сессии mSession. Как вы понимаете, на новую инициализацию камеры требуется время.
Вторая — основная задержка по времени получалась из-за того, что остановка медиарекордера mMediaRecorder.stop(), оказывается, выполняется совсем не асинхронно, как я наивно надеялся, а вполне себе вешает главный поток. И по идее, должна быть вынесена в фон. Но на самом деле и фон тут не поможет, так как пересоздание mSession все равно требует нового экземпляра mMediaRecorder, а мы его никак не получим, пока не остановим старый. Конечно, можно попробовать запускать некий mMediaRecorder2, пока закрываем mMediaRecorder1, но это подход индусский и ненадежный, так как:

Поведение нескольких экземпляров MediaRecorder является неопределенным afaik и может работать или не работать в зависимости от устройства.

В документации не упоминается, поддерживаются ли несколько экземпляров:

developer.android.com/reference/android/media/MediaRecorder.html

Есть жалобы о сбоях при создании экземпляров более одного экземпляра.


Может это и до сих пор так, а может ситуация и изменилась с тех пор, но в любом случае выход есть. Правда, с другой стороны.

Мы попробуем не закрывать CameraPreviewSession() каждый раз, когда требуется новая запись. Но в этом случае при остановке медиарекордера, она закроется сама, причем вместе с приложением, издевательски написав, что «потеряна Surface вашего медиарекордера». Что, как бы и логично, мы же его сами остановили и закрыли.

Поэтому здесь нам надо использовать другой Surface — по латыни PersistentInputSurface() или «сурфейс упоротый упорный». Он не закроется при останове медиарекордера, а будет себе жить дальше. Правда, он требует более специфического обращения (см. комментарии к статье), но зато работает и работает без лагов!

Вариант кода без лагов
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.media.MediaCodec;
import android.os.AsyncTask;
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 {

    Surface recorderSurface =null;
    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();
                    Log.i(LOG_TAG, "START");

                }
            }
        });


        mButtonStopRecordVideo.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if ((myCameras[CAMERA1] != null) & (mMediaRecorder != null)) {
                    myCameras[CAMERA1].stopRecordingVideo();
                    Log.i(LOG_TAG, "STOP");
                }


            }
        });


        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.setVideoSize(640, 480);
        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);





    }

    private void setUpMediaRecorder2() {

        mMediaRecorder = new MediaRecorder();

        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);

        mMediaRecorder.setInputSurface(recorderSurface);


        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mCurrentFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test"+count+".mp4");
        mMediaRecorder.setOutputFile(mCurrentFile.getAbsolutePath());
        mMediaRecorder.setVideoSize(640, 480);
        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, " запустили медиа рекордер2");

        } 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_PREVIEW);

                /**Surface for the camera preview set up*/

                mPreviewBuilder.addTarget(surface);

                /**MediaRecorder setup for surface*/


              //  Surface recorderSurface = mMediaRecorder.getSurface();

                recorderSurface=MediaCodec.createPersistentInputSurface();
                mMediaRecorder.setInputSurface(recorderSurface);



                try {
                    mMediaRecorder.prepare();
                    Log.i(LOG_TAG, " запустили медиа рекордер");

                } catch (Exception e) {
                    Log.i(LOG_TAG, "не запустили медиа рекордер");
                }
                mPreviewBuilder.addTarget(recorderSurface);

                mCameraDevice.createCaptureSession(Arrays.asList(surface,recorderSurface),
                        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() {




            count++;
            MyTask mt = new MyTask();
            mt.execute();


        }


        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();

    }


    class MyTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected void onPreExecute() {

        }

        @Override
        protected Void doInBackground(Void... params) {
            mMediaRecorder.stop();
            mMediaRecorder.release();
            setUpMediaRecorder2();
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {

        }
    }




}



Комментарии 25

    0
    поэтому, если неразумно тыкать в экранные кнопки в рандомном порядке, то можно всё сломать

    При блокировке экрана как себя поведет?
      0
      del
        0
        Да особо и не тестировал. Собственно, это для демонстрации работы — tutorial. Тут, чем проще тем лучше.
          0
          До этого момента не задумывался о существования софта, который мог бы как регистратор записывать, экономя энергию на выключенном экране. Т.е. насколько проблема для программы работать при заблокированном экране в последних версиях Android
            +1
            А, понил. Если начать запись и только выключить экран, то на удивление, но продолжает писать видео в файл.
            Если начать запись и свернуть приложение, то нет (как и полагается)
            У меня Android 9 Pie
        0
        А как можно остановить медиарекордер без пересоздания всей сессии превью? Просто в этот момент видимый лаг случается… Если же делать просто остановку медиарекордера, то стопается и само превью…
          0
          по идее не должно, так как оно теоретически по разным потокам разбросано, то есть медиарекордер работает независимо (можно делать например так скрытую съемку, без превью). Почему лаг, шиш его знает.
          В данной демонстрашке нет корректного закрытия камеры, может поэтому крашится.

            0
            Лаг не на UI, а у самой камеры(на превью). Если просто стопнуть медиарекордер, то surface, которую он отдал в CaptureSession будет не валидна, если менять сессию — то видна задержка переключения.
            В стандартной камере как-то сделано, что при остановке записи превью не замирает на секунду.
              0
              Может они на чем-то более низкоуровневом стандартную камеру пишут? Вообще без surface концепций.
                0
                Конечно, думаю через тот же MediaCodec и муксер.
                Но перспектива переделки на это сомнительна ради одной мелкой задержки)
                  0
                  Вроде как делается через persistant input surface, которая типа не отваливается. Но на сайте андроида сказано, что ее надо делать перед prepare() рекордера и не использовать тогда getSurface.
                  Но как это привязать к коду и в каком месте вообще тогда непонятно.
                    0
                    В целом там всё понятно, она создаётся и добавляет как в рекордер так и в таргеты при создании CameraSession.
                    Но у меня не получилось её заиспользовать всё равно)) Почему уже не помню)
                      0
                      Так а как конкретно привязать рекордер к persistant input surface? Я не нашел примеров.
                        0
                        Посмотрите джавадоки для MediaRecorder.setInputSurface
                        Там описано примерно. Создать Mediacodec.createPersistentInputSurface()
                        Установить его в MediaRecorder.setInputSurface() и в таргеты в КамераСессион.
          0
          Surface recorderSurface=MediaCodec.createPersistentInputSurface();
          mMediaRecorder.setInputSurface(recorderSurface);

          матерится на вторую строчку. пишет

          setInputSurface is called in an invalid state: 8
            0
            @throws IllegalStateException if it is called after {@link #prepare} and before {@link #stop}.

            Может из-за этого?
            0
            Точно.
              0
              Если получится такой способ, то отпишитесь)
                0
                Так вроде теперь работает. Единственно что каждый новый экземпляр рекордера (после стопа предыдущего) теряет связь с этой PersistantSurface (поскольку она уже была определена в сессии), поэтому я сделал PersistantSurface статической, чтобы новый рекордер мог к ней заново прицепится через mMediaRecorder.setInputSurface(recorderSurface);.

                Но смех в том, что лаг хоть и уменьшился, но остался. А я еще удивлялся, неужели камера открывается полторы секунды?
                Короче главный тормоз это вот:
                Slow Looper main: doFrame is 915ms late because of 2 msg, msg 1 took 912ms (h=android.view.ViewRootImpl$ViewRootHandler c=android.view.View$PerformClick

                Может это связано с тем, что все творится в главном потоке? Но там вроде
                коллбэки везде. Короче хз.
                Причем это же и было в исходной версии, где стопалось превью, просто
                я не обратил на это внимания, думая что дело в задержке из-за нового превью.

                  0
                  А стоп рекордера делаете на главном потоке?
                  Если да, то можно вынести его в фон.
                    0
                    А тогда целиком приложение падает. Я и asynTask делал и просто поток запускал джавовский.
                      0
                      Забыл, оказывается, в фон вынести инициализацию нового рекордера. Поэтому падало. Его надо после стопанья делать. А получалось до.
                      Так что теперь работает без лагов:)
                        0
                        Где-то исходники есть?)
                          +1
                          Ага, закинул в конец статьи.
              0
              И эту часть на аглицкий перевели! Во, как!

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

              Самое читаемое