- PVSM.RU - https://www.pvsm.ru -

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP

Итак, со съемками фоточек и записью видео при помощи Camera2 API мы вроде бы, разобрались. Осталось только научиться передавать видеопоток c Android устройства страждущим получателям извне. Конечной целью, как уже неоднократно ранее говорилось, является интеллектуализация роботелеги — ставим на неё смартфон и так сказать, превращаем обезьяну в человека. В этом нам поможет Media Codec. И конечно, новое Camera2 API.

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 1

Кому интересно, прошу под кат.

Детали о проекте с роботележкой можно найти здесь [1], а мы пока займемся непосредственно стримингом видео с неё (вернее с прицепленного к ней Android смартфона) на персональную электронно-вычислительную машину.

Что нам для этого нужно?

Для того чтобы передать видео поток с экрана смартфона куда-либо ещё, как известно, его (поток) сначала необходимо преобразовать в подходящий ужатый формат (передавать покадрово выйдет слишком толсто), поставить time-stamps (временные метки) и отправить в бинарном виде получателю. Который произведёт обратную операцию декодирования.

Как раз этими низкоуровневыми чёрными делами и занимается класс Media Codec с 2013 года, с даты выхода Аndroid 4.3.

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 2

Другое дело, что раньше подступиться к кодированию видео, в отличие от сегодняшнего дня было не так-то просто. Чтобы вытащить картинку с камеры надо было использовать тонны загадочного кода [2] в котором, как в заклинаниях якутских шаманов, единственная неточность могла привести к полному краху приложения. Добавьте к этому ещё предыдущее Camera API, где вместо готовых коллбэков приходилось ручками самостоятельно писать разные synchronized, а это занятие, скажем так, не для слабых духом.

И главное, издалека смотришь на рабочий код [3], вроде бы в общих чертах всё ясно. Начинаешь переносить по частям в свой проект — сыпется непонятно почему. А скорректировать не получается, потому что в деталях уже разобраться тяжело.

Да и от сплошных deprecated как-то не по себе. Короче говоря, непорядок

К счастью, для непонятливых гуглостроители ввели волшебную концепцию поверхности Surface, работая с которой, можно избежать низкоуровневых деталей. Какой ценой и что при этом теряет разработчик, мне как неспециалисту понять сложно, но зато теперь мы чуть ли не буквально можем сказать: «Android, возьми эту Surface на которую отображается видео с камеры и ничего там не меняя, ну вот как есть, закодируй и отправь дальше». И самое удивительное, что это работает. А с новым Camera2 API программа и сама знает, когда данные отправлять, коллбэки ж новые появились!

Так что теперь закодировать видео — раз плюнуть. Чем мы сейчас и займёмся.
Берём код из первой статьи [4] и как обычно выкидываем из него всё кроме кнопочек и инициализации камеры.

Начнём с 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>

И закончим прицеплением Media Codec

В прошлом посте мы выводили на Surface изображение с камеры и с него же писали видео при помощи MediaRecorder. Для этого мы просто указывали оба компонента в списке Surface.

(Arrays.asList(surface, mMediaRecorder.getSurface()).

Здесь то же самое, только вместо mMediaRecorder указываем:

(Arrays.asList(surface, mEncoderSurface),

Получается, что-то типа:

   private void startCameraPreviewSession() {
            SurfaceTexture texture = mImageView.getSurfaceTexture();
            texture.setDefaultBufferSize(320, 240);
            surface = new Surface(texture);


            try {

                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                mPreviewBuilder.addTarget(surface);
                mPreviewBuilder.addTarget(mEncoderSurface);


                mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface),

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

        }

Что такое mEncoderSurface? А это та самая Surface, с которой будет работать Media Codec. Только для начала надо их обоих инициализировать примерно таким образом.

 private void setUpMediaCodec() {



        try {
            mCodec = MediaCodec.createEncoderByType("video/avc"); // H264 кодек

        } catch (Exception e) {
            Log.i(LOG_TAG, "а нету кодека");
        }

        int width = 320; // ширина видео
        int height = 240; // высота видео
        int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета
        int videoBitrate = 500000; // битрейт видео в bps (бит в секунду)
        int videoFramePerSecond = 20; // FPS
        int iframeInterval = 2; // I-Frame интервал в секундах

        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);


        mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер
        mEncoderSurface = mCodec.createInputSurface(); // получаем Surface кодера

        mCodec.setCallback(new EncoderCallback());
        mCodec.start(); // запускаем кодер
        Log.i(LOG_TAG, "запустили кодек");

    }

Теперь остается прописать единственный коллбэк. Когда Media Codec вдруг ощутит, что очередные данные для дальнейшей трансляции готовы, он нас об этом известит именно через него:

    private class EncoderCallback extends MediaCodec.Callback {

        @Override
        public void onInputBufferAvailable(MediaCodec codec, int index) {

        }

        @Override
        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {


            outPutByteBuffer = mCodec.getOutputBuffer(index);
            byte[] outDate = new byte[info.size];
            outPutByteBuffer.get(outDate);


         
            Log.i(LOG_TAG, " outDate.length : " + outDate.length);
   

            mCodec.releaseOutputBuffer(index, false);


        }

        @Override
        public void onError(MediaCodec codec, MediaCodec.CodecException e) {
            Log.i(LOG_TAG, "Error: " + e);
        }

        @Override
        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
            Log.i(LOG_TAG, "encoder output format changed: " + format);
        }
    }

Байтовый массив outDate — это настоящее сокровище. В нём уже готовые кусочки закодированного в формате H264 видеопотока, с которым мы теперь можем делать всё, что захотим.

Вот они…

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 3

Некоторые кусочки может быть великоваты для передачи по сети, ну да ничего, система, если надо их порубит ещё сама и отправит дальше получателю.

Но нам пока надо убедиться воочию, что данные в буфере это действительно видеопоток в формате H264. Поэтому, давайте мы их отправим в файл:

Пропишем в сетапе:

 private void setUpMediaCodec() {

 File mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test3.h264");

        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(mFile));
            Log.i("Encoder", "outputStream initialized");
        } catch (Exception e) {
            e.printStackTrace();
        }

А в коллбэке где буфер:


     try {
                
                outputStream.write(outDate, 0, outDate.length);// гоним байты в поток
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

Открываем приложение, жмем кнопку: «ВКЛЮЧИТЬ КАМЕРУ И СТРИМ». Начинается автоматически запись. Ждем немного и давим кнопку остановки.

Сохраненный файл штатно скорее всего не проиграется, поскольку формат не MP4, но если открыть его VLC плеером или сконвертить онлайн каким нибудь ONLINE CONVERT [5], то мы убедимся, что находимся на правильном пути. Правда изображение лежит на боку, но это поправимо.

Вообще, для каждого события записи, фотографирования или стрима, лучше, конечно, открывать каждый раз новую сессию, а старую закрывать. То есть, сначала мы включаем камеру и запускаем голое превью. Потом, если надо сделать снимок, превью закрываем и открываем превью, но уже с пристегнутым Image Reader. Если переходим на запись видео, то закрываем текущую сессию и запускаем сессию с превью и прицепленным к нему Media Recorder. Я этого не делал, чтобы не страдала наглядность кода, а вам решать, как удобнее самим.

А вот и сам код целиком.

BasicMediaCodec

package com.example.basicmediacodec;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Context;
import android.content.pm.ActivityInfo;
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.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;

import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.StrictMode;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;


import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;


public class MainActivity extends AppCompatActivity {


    public static final String LOG_TAG = "myLogs";
    public static Surface surface = null;

    CameraService[] myCameras = null;

    private CameraManager mCameraManager = null;
    private final int CAMERA1 = 0;


    private Button mButtonOpenCamera1 = null;
    private Button mButtonStreamVideo = null;
    private Button mButtonTStopStreamVideo = null;
    public static TextureView mImageView = null;
    private HandlerThread mBackgroundThread;
    private Handler mBackgroundHandler = null;

    private MediaCodec mCodec = null; // кодер
    Surface mEncoderSurface; // Surface как вход данных для кодера
    BufferedOutputStream outputStream;
    ByteBuffer outPutByteBuffer;


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


    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    protected void onCreate(Bundle savedInstanceState) {


        super.onCreate(savedInstanceState);

     
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        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)
        ) {
            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        }


        mButtonOpenCamera1 = findViewById(R.id.button1);
        mButtonStreamVideo = findViewById(R.id.button2);
        mButtonTStopStreamVideo = findViewById(R.id.button3);
        mImageView = findViewById(R.id.textureView);

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


                setUpMediaCodec();// инициализируем Медиа Кодек

                if (myCameras[CAMERA1] != null) {// открываем камеру
                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();
                }


            }
        });

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

                {
                      //  тут пока пусто

                }

               }
        });


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


                if (mCodec != null) {

                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();
                    myCameras[CAMERA1].stopStreamingVideo();
                }


            }
        });


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

            @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(320, 240);
            surface = new Surface(texture);


            try {

                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                mPreviewBuilder.addTarget(surface);
                mPreviewBuilder.addTarget(mEncoderSurface);


                mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface),

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

            }
        }

        public void closeCamera() {

            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }

        public void stopStreamingVideo() {

            if (mCameraDevice != null & mCodec != null) {

                try {
                    mSession.stopRepeating();
                    mSession.abortCaptures();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }

                mCodec.stop();
                mCodec.release();
                mEncoderSurface.release();
                closeCamera();
            }
        }
    }


    private void setUpMediaCodec() {


        File mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test3.h264");

        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(mFile));
            Log.i("Encoder", "outputStream initialized");
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            mCodec = MediaCodec.createEncoderByType("video/avc"); // H264 кодек

        } catch (Exception e) {
            Log.i(LOG_TAG, "а нету кодека");
        }

        int width = 320; // ширина видео
        int height = 240; // высота видео
        int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета
        int videoBitrate = 500000; // битрейт видео в bps (бит в секунду)
        int videoFramePerSecond = 20; // FPS
        int iframeInterval = 3; // I-Frame интервал в секундах

        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);


        mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер
        mEncoderSurface = mCodec.createInputSurface(); // получаем Surface кодера

        mCodec.setCallback(new EncoderCallback());
        mCodec.start(); // запускаем кодер
        Log.i(LOG_TAG, "запустили кодек");

    }

    private class EncoderCallback extends MediaCodec.Callback {

        @Override
        public void onInputBufferAvailable(MediaCodec codec, int index) {

        }

        @Override
        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {


            outPutByteBuffer = mCodec.getOutputBuffer(index);
            byte[] outDate = new byte[info.size];
            outPutByteBuffer.get(outDate);


            try {
                Log.i(LOG_TAG, " outDate.length : " + outDate.length);
                outputStream.write(outDate, 0, outDate.length);// гоним байты в поток
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            mCodec.releaseOutputBuffer(index, false);


        }

        @Override
        public void onError(MediaCodec codec, MediaCodec.CodecException e) {
            Log.i(LOG_TAG, "Error: " + e);
        }

        @Override
        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
            Log.i(LOG_TAG, "encoder output format changed: " + format);
        }
    }

    @Override
    public void onPause() {
        if (myCameras[CAMERA1].isOpen()) {
            myCameras[CAMERA1].closeCamera();
        }

        stopBackgroundThread();
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();
    }
}

И не забудьте про разрешения в манифесте.

 <uses-permission android:name="android.permission.CAMERA" />
 <uses-permission android:name="android.permission.INTERNET"/>

Итак, мы убедились, что Media Codec работает. Но использовать его для записи видео в файл как-то бездуховно. С такой задачей гораздо лучше справится Media Recorder, да ещё и звук добавит. Поэтому файловую часть мы снова выкинем и добавим блок кода для стриминга видео в сеть по udp протоколу. Это тоже очень просто.

Сначала инициализируем UDP практически сервер.

  DatagramSocket udpSocket;
    String ip_address = "192.168.1.84"; // целевой адрес куда пуляем видео
    InetAddress address; 
    int port = 40002;  // порт, который выставит получатель для прослушивания

……..

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

                try {
                    udpSocket = new DatagramSocket();

                    Log.i(LOG_TAG, "  создали udp сокет");

                } catch (
                        SocketException e) {
                    Log.i(LOG_TAG, " не создали udp сокет");
                }

                try {
                    address = InetAddress.getByName(ip_address);
                    Log.i(LOG_TAG, "  есть адрес");
                } catch (Exception e) {

А в том же коллбэке, где мы отправляли данные по готовности в поток для файла, отправим их теперь в виде дэйтаграмм в нашу домашнюю сеть (надеюсь она есть у всех?)

  try {
                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);
                udpSocket.send(packet);
            } catch (IOException e) {
                Log.i(LOG_TAG, " не отправился UDP пакет");
            }

И все?

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

 StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);

В общем и целом, получится следующая маленькая элегантная демонстрационная программка:

package com.example.basicmediacodec;


import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Context;
import android.content.pm.ActivityInfo;
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.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;


import android.os.Handler;
import android.os.HandlerThread;
import android.os.StrictMode;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;


import java.io.BufferedOutputStream;


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;


public class MainActivity extends AppCompatActivity {


    public static final String LOG_TAG = "myLogs";
    public static Surface surface = null;

    CameraService[] myCameras = null;

    private CameraManager mCameraManager = null;
    private final int CAMERA1 = 0;

    private Button mButtonOpenCamera1 = null;
    private Button mButtonStreamVideo = null;
    private Button mButtonTStopStreamVideo = null;
    public static TextureView mImageView = null;
    private HandlerThread mBackgroundThread;
    private Handler mBackgroundHandler = null;

    private MediaCodec mCodec = null; // кодер
    Surface mEncoderSurface; // Surface как вход данных для кодера
    BufferedOutputStream outputStream;
    ByteBuffer outPutByteBuffer;
    DatagramSocket udpSocket;
    String ip_address = "192.168.1.84";
    InetAddress address;
    int port = 40002;

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


    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    protected void onCreate(Bundle savedInstanceState) {


        super.onCreate(savedInstanceState);

        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        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)
        ) {
            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        }

        mButtonOpenCamera1 = findViewById(R.id.button1);
        mButtonStreamVideo = findViewById(R.id.button2);
        mButtonTStopStreamVideo = findViewById(R.id.button3);
        mImageView = findViewById(R.id.textureView);

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

                try {
                    udpSocket = new DatagramSocket();

                    Log.i(LOG_TAG, "  создали udp сокет");

                } catch (
                        SocketException e) {
                    Log.i(LOG_TAG, " не создали udp сокет");
                }

                try {
                    address = InetAddress.getByName(ip_address);
                    Log.i(LOG_TAG, "  есть адрес");
                } catch (Exception e) {


                }
                setUpMediaCodec();// инициализируем Медиа Кодек

                if (myCameras[CAMERA1] != null) {// открываем камеру
                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();
                }
            }
        });

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

                {
                      //  тут пока пусто

                }
               }
        });


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


                if (mCodec != null) {

                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();
                    myCameras[CAMERA1].stopStreamingVideo();
                }
            }
        });


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

            @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(320, 240);
            surface = new Surface(texture);


            try {

                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                mPreviewBuilder.addTarget(surface);
                mPreviewBuilder.addTarget(mEncoderSurface);


                mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface),

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

            }
        }

        public void closeCamera() {

            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }

        public void stopStreamingVideo() {

            if (mCameraDevice != null & mCodec != null) {

                try {
                    mSession.stopRepeating();
                    mSession.abortCaptures();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }

                mCodec.stop();
                mCodec.release();
                mEncoderSurface.release();
                closeCamera();
            }
        }
    }


    private void setUpMediaCodec() {




        try {
            mCodec = MediaCodec.createEncoderByType("video/avc"); // H264 кодек

        } catch (Exception e) {
            Log.i(LOG_TAG, "а нету кодека");
        }

        int width = 320; // ширина видео
        int height = 240; // высота видео
        int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета
        int videoBitrate = 500000; // битрейт видео в bps (бит в секунду)
        int videoFramePerSecond = 20; // FPS
        int iframeInterval = 3; // I-Frame интервал в секундах

        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);


        mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер
        mEncoderSurface = mCodec.createInputSurface(); // получаем Surface кодера

        mCodec.setCallback(new EncoderCallback());
        mCodec.start(); // запускаем кодер
        Log.i(LOG_TAG, "запустили кодек");

    }

    private class EncoderCallback extends MediaCodec.Callback {

        @Override
        public void onInputBufferAvailable(MediaCodec codec, int index) {

        }

        @Override
        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {


            outPutByteBuffer = mCodec.getOutputBuffer(index);
            byte[] outDate = new byte[info.size];
            outPutByteBuffer.get(outDate);

            try {
                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);
                udpSocket.send(packet);
            } catch (IOException e) {
                Log.i(LOG_TAG, " не отправился UDP пакет");
            }


            mCodec.releaseOutputBuffer(index, false);


        }

        @Override
        public void onError(MediaCodec codec, MediaCodec.CodecException e) {
            Log.i(LOG_TAG, "Error: " + e);
        }

        @Override
        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
            Log.i(LOG_TAG, "encoder output format changed: " + format);
        }
    }

    @Override
    public void onPause() {
        if (myCameras[CAMERA1].isOpen()) {
            myCameras[CAMERA1].closeCamera();
        }

        stopBackgroundThread();
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();
    }
}

Не знаю, как у других, но на моем Red Note 7 даже видно, как скачут килобайты по нужному адресу

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 4

И таких udp сокетов можно наплодить множество, на сколько хватит пропускной способности сети. Главное, чтобы были адреса куда. Будет у вас широкоадресная рассылка.

Теперь пойдём искать нужный адрес на компьютере

Надо сказать, что не каждая компьютерная программа способна всосать и переварить видео поток формата H264 по единственному udp каналу без какой-либо дополнительной информации. Но некоторые могут. Это например крайне широко известный медиаплеер VLC [6]. Это настолько крутая штука, что если начать описывать её возможности, то из статьи получится целая книга. Наверняка у вас она есть. Если нет, поставьте.

И судя по описанию команд для него, udp пакеты переварить этот плеер может.

URL syntax:

  file:///path/file              Plain media file
  http://host[:port]/file        HTTP URL
  ftp://host[:port]/file         FTP URL
  mms://host[:port]/file         MMS URL
  screen://                      Screen capture
  dvd://[device]                 DVD device
  vcd://[device]                 VCD device
  cdda://[device]                Audio CD device
  udp://[[<source address>]@[<bind address>][:<bind port>]]

Причём все эти source address и bind address, по идее и не нужны. Нужен только прослушиваемый порт.

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 5

И ещё, конечно, нужно не забыть про разрешение этот порт слушать (малварь же)

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 6

А вы знали, что Винда не дает сделать принтскрин с монитора ресурсов?

Или можно вообще брандмауэр отключить (не рекомендую)

Итак, преодолев эти тернии, запускаем VLC плеер с нашим адресом и наслаждаемся пустым экраном. Видео нет.

Как же так?

А вот так. У вас, наверно, стоит последняя версия VLC 3.08 Vetinari? Вот как раз, в этой версии udp объявлен deprecated и мало того выпилен нахрен.

Так-то логика разработчиков плеера понятна. Мало кому нужно использовать голый udp канал в наше время потому-что:

  • Нормально работает только в домашней неперегруженной сети. Стоит вам выйти во внешний мир и ненумерованные дэйтаграммы начнут теряться и приходить не в том порядке, в котором их послали. А для видео декодера это очень неприятно.
  • Незашифрован и легко компрометируется

Поэтому нормальные люди, конечно, используют протоколы более высокого уровня RTC и другие. То есть на пальцах — вы пишете сервер, который всё равно на низком уровне использует udp (для скорости), но параллельно обменивается управляющей информацией с клиентом кому он стримит видео. Какая там у него пропускная способность, не надо ли увеличить-уменьшить кэш для данных, какая детализация изображения оптимальна сейчас и так далее и тому подобное. Опять же звук тоже иногда нужен. А ему требуется, сами понимаете, синхронизация с видео.

Вон ребятам из Одноклассников даже пришлось свой протокол [7] запилить для стриминга. Но у них задачи-то, конечно, гораздо более важные — рассылать видео с котиками десяткам миллионов домохозяек по всему миру. Там одним udp каналом не обойдёшься.
Но нам-то писать свой RTP сервер на андроиде как-то грустно. Наверное, можно найти даже готовый и даже бесплатный, но попробуем пока не усложнять сущностей. Просто возьмем версию VLC плеера, где udp стриминг ещё работал.

Итак, качаем отсюда VLC 2.2.6 Umbrella [8]

Устанавливаем вместо или рядом со старым (то есть новым VLC), как вам заблагорассудится.

Запускаем и снова видим пустой экран.

А все это потому, что мы явно не настроили использование кодека H264. Так-то VLC смог бы выбрать кодек автоматически, если бы имел дело с файлом (в настройках изначально, как раз и указан автоматический выбор). Но ему-то кидают байтовый поток по единственному каналу, а кодеков, которые VLC поддерживает десятки. Как ему разобраться, какой применить?

Поэтому устанавливаем кодек силой.

Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP - 7

И вот теперь наслаждаемся трансляциией «живого» видео. Единственное, оно зачем-то лежит на боку, но это уже легко поправить в настройках видеоплеера.

А ещё можно просто запускать плеер из командной строки по такому ключу:

C:Program FilesVideoLANVLCvlc udp://@:40002   --demux h264 --video-filter=transform --transform-type=90

И раскодирует сам и повернет.

Итак стриминг работает. Осталось только интегрировать его в окошко JAVA приложения для управления роботележкой. Займемся этим очень скоро в финальной части.

Автор: Tarson

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/334774

Ссылки в тексте:

[1] здесь: https://habr.com/ru/users/tarson/posts/

[2] загадочного кода: https://bigflake.com/mediacodec/CameraToMpegTest.java.txt

[3] код: https://github.com/bytestar/android-h264-stream-demo/blob/master/AndroidH264StreamDemo/src/com/jemi/androidh264streamdemo/AvcEncoder.java

[4] первой статьи: https://habr.com/ru/post/468083/

[5] ONLINE CONVERT: https://video.online-convert.com/ru/convert-to-mp4

[6] VLC: https://www.videolan.org/vlc%2F

[7] протокол: https://habr.com/ru/company/odnoklassniki/blog/467669/

[8] VLC 2.2.6 Umbrella : http://download.videolan.org/pub/videolan/vlc/2.2.6/win64/

[9] Источник: https://habr.com/ru/post/473036/?utm_source=habrahabr&utm_medium=rss&utm_campaign=473036