Как стать автором
Обновить

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

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


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

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

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


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

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



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

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

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

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

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

Начнём с 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 видеопотока, с которым мы теперь можем делать всё, что захотим.

Вот они…



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

            int temp =outDate.length ;

            do {//кромсаем на небольше килобайта

                byte[] ds;

                temp = temp-1024;

                if(temp>=0)
                { ds = new byte[1024];}
                else
                { ds = new byte[temp+1024];}


                for(int i =0;i<ds.length;i++)
                {
                    ds[i]=outDate[i+1024*count];

                }

                count=count+1;


              
            }
            while (temp>=0);



Но нам пока надо убедиться воочию, что данные в буфере это действительно видеопоток в формате 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, то мы убедимся, что находимся на правильном пути. Правда изображение лежит на боку, но это поправимо.

Вообще, для каждого события записи, фотографирования или стрима, лучше, конечно, открывать каждый раз новую сессию, а старую закрывать. То есть, сначала мы включаем камеру и запускаем голое превью. Потом, если надо сделать снимок, превью закрываем и открываем превью, но уже с пристегнутым 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;  // порт, который выставит получатель для прослушивания

……..

  

                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) {

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

      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) {


                }



        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 даже видно, как скачут килобайты по нужному адресу



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

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


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

И судя по описанию команд для него, 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, по идее и не нужны. Нужен только прослушиваемый порт.



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



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

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

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

Как же так?


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

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

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

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

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

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

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

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

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

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



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

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

C:\Program Files\VideoLAN\VLC\vlc udp://@:40002   --demux h264 --video-filter=transform --transform-type=90

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


Итак стриминг работает. Осталось только интегрировать его в окошко JAVA приложения на компьютере для управления роботележкой. Займемся этим очень скоро в финальной части.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+19
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн