- PVSM.RU - https://www.pvsm.ru -
Итак, со съемками фоточек и записью видео при помощи Camera2 API мы вроде бы, разобрались. Осталось только научиться передавать видеопоток c Android устройства страждущим получателям извне. Конечной целью, как уже неоднократно ранее говорилось, является интеллектуализация роботелеги — ставим на неё смартфон и так сказать, превращаем обезьяну в человека. В этом нам поможет Media Codec. И конечно, новое Camera2 API.
Кому интересно, прошу под кат.
Детали о проекте с роботележкой можно найти здесь [1], а мы пока займемся непосредственно стримингом видео с неё (вернее с прицепленного к ней Android смартфона) на персональную электронно-вычислительную машину.
Для того чтобы передать видео поток с экрана смартфона куда-либо ещё, как известно, его (поток) сначала необходимо преобразовать в подходящий ужатый формат (передавать покадрово выйдет слишком толсто), поставить time-stamps (временные метки) и отправить в бинарном виде получателю. Который произведёт обратную операцию декодирования.
Как раз этими низкоуровневыми чёрными делами и занимается класс Media Codec с 2013 года, с даты выхода Аndroid 4.3.
Другое дело, что раньше подступиться к кодированию видео, в отличие от сегодняшнего дня было не так-то просто. Чтобы вытащить картинку с камеры надо было использовать тонны загадочного кода [2] в котором, как в заклинаниях якутских шаманов, единственная неточность могла привести к полному краху приложения. Добавьте к этому ещё предыдущее Camera API, где вместо готовых коллбэков приходилось ручками самостоятельно писать разные synchronized, а это занятие, скажем так, не для слабых духом.
И главное, издалека смотришь на рабочий код [3], вроде бы в общих чертах всё ясно. Начинаешь переносить по частям в свой проект — сыпется непонятно почему. А скорректировать не получается, потому что в деталях уже разобраться тяжело.
Да и от сплошных deprecated как-то не по себе. Короче говоря, непорядок
К счастью, для непонятливых гуглостроители ввели волшебную концепцию поверхности Surface, работая с которой, можно избежать низкоуровневых деталей. Какой ценой и что при этом теряет разработчик, мне как неспециалисту понять сложно, но зато теперь мы чуть ли не буквально можем сказать: «Android, возьми эту Surface на которую отображается видео с камеры и ничего там не меняя, ну вот как есть, закодируй и отправь дальше». И самое удивительное, что это работает. А с новым Camera2 API программа и сама знает, когда данные отправлять, коллбэки ж новые появились!
Так что теперь закодировать видео — раз плюнуть. Чем мы сейчас и займёмся.
Берём код из первой статьи [4] и как обычно выкидываем из него всё кроме кнопочек и инициализации камеры.
<?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>
В прошлом посте мы выводили на 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 видеопотока, с которым мы теперь можем делать всё, что захотим.
Вот они…
Некоторые кусочки может быть великоваты для передачи по сети, ну да ничего, система, если надо их порубит ещё сама и отправит дальше получателю.
Но нам пока надо убедиться воочию, что данные в буфере это действительно видеопоток в формате 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. Я этого не делал, чтобы не страдала наглядность кода, а вам решать, как удобнее самим.
А вот и сам код целиком.
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 даже видно, как скачут килобайты по нужному адресу
И таких 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, по идее и не нужны. Нужен только прослушиваемый порт.
И ещё, конечно, нужно не забыть про разрешение этот порт слушать (малварь же)
А вы знали, что Винда не дает сделать принтскрин с монитора ресурсов?
Или можно вообще брандмауэр отключить (не рекомендую)
Итак, преодолев эти тернии, запускаем 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 поддерживает десятки. Как ему разобраться, какой применить?
Поэтому устанавливаем кодек силой.
И вот теперь наслаждаемся трансляциией «живого» видео. Единственное, оно зачем-то лежит на боку, но это уже легко поправить в настройках видеоплеера.
А ещё можно просто запускать плеер из командной строки по такому ключу:
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
Нажмите здесь для печати.