API Android Camera2 à partir de la théière, partie 3. Codec multimédia et streaming vidéo sur UDP

Donc, nous avons en quelque sorte compris comment prendre des photos et enregistrer des vidéos en utilisant l'API Camera2. Il ne reste plus qu'à apprendre à transférer le flux vidéo de l'appareil Android vers les destinataires souffrant de l'extérieur. Le but ultime, comme cela a été dit à plusieurs reprises, est l'intellectualisation des robots - nous y mettons un smartphone et, pour ainsi dire, transformons un singe en une personne. Media Codec nous y aidera. Et bien sûr, la nouvelle API Camera2.


Peu importe, s'il vous plaît, sous le chat.

Les détails sur le projet robotique peuvent être trouvés ici , mais pour l' instant , nous allons directement diffuser la vidéo depuis celui-ci (ou plutôt, depuis un smartphone Android connecté) vers un ordinateur électronique personnel.

De quoi avons-nous besoin pour cela?


Afin de transférer un flux vidéo de l'écran du smartphone vers un autre endroit, comme vous le savez, il (flux) doit d'abord être converti en un format réduit approprié (il sera trop épais pour transmettre image par image), mettre des horodatages (horodatages) et envoyer sous forme binaire au destinataire . Qui effectuera l'opération de décodage inverse.

Ce sont précisément ces actes noirs de bas niveau que la classe Media Codec traite depuis 2013, à partir de la date de sortie d'Android 4.3.



Une autre chose est que l'approche du codage vidéo antérieure, contrairement à aujourd'hui, n'était pas si simple. Pour obtenir une image de la caméra, il a fallu utiliser des tonnes d'un code mystérieux dans lequel, comme dans les sorts des chamans Yakut, la seule inexactitude pouvait conduire à un crash complet de l'application. Ajoutez à cela l'API Camera précédente, où au lieu de rappels prêts à l'emploi, vous deviez écrire vous-même différents stylets synchronisés, et cette activité, disons, n'est pas pour les âmes sensibles.

Et surtout, vous regardez le code de travail de loin, tout semble clair en termes généraux. Vous commencez à transférer en parties à votre projet - il n'est pas clair pourquoi il coule. Mais il est impossible de corriger, car il est difficile de comprendre les détails.

Oui, et de solide obsolète en quelque sorte à l'aise. En bref, le désordre

Heureusement, pour les esprits lents, les constructeurs de Google ont introduit le concept magique de Surface , avec lequel vous pouvez éviter les détails de bas niveau. C'est difficile pour moi en tant que profane de comprendre à quel prix et ce que le développeur perd, mais maintenant nous pouvons presque littéralement dire: "Android, prenez cette Surface sur laquelle la vidéo de la caméra est affichée et ne changez rien là, eh bien, comme c'est le cas, encodez et envoyer. " Et le plus étonnant, c'est que ça marche. Et avec la nouvelle API Camera2, le programme lui-même sait quand envoyer des données, de nouveaux rappels sont apparus!

Alors maintenant pour encoder la vidéo - il suffit de cracher. Que ferons-nous maintenant.
Nous prenons le code du premier article et, comme d'habitude, jetons tout hors de lui sauf les boutons et l'initialisation de la caméra.

Commençons par la mise en page de l'application.
<?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> 


Et terminez avec le raccordement Media Codec


Dans le dernier article, nous avons affiché l'image de la caméra sur la Surface et écrit une vidéo à partir de celle-ci à l'aide de MediaRecorder. Pour ce faire, nous avons simplement spécifié les deux composants dans la liste Surface.

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

Ici, la même chose, seulement au lieu de mMediaRecorder, nous spécifions:

 (Arrays.asList(surface, mEncoderSurface), 

Il s'avère que quelque chose comme:

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

Qu'est-ce que mEncoderSurface? Et c'est la même surface avec laquelle Media Codec fonctionnera. Pour commencer, vous devez les initialiser tous les deux approximativement de cette façon.

  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, " "); } 

Reste maintenant à enregistrer un seul rappel. Lorsque Media Codec sent soudain que les prochaines données pour une diffusion ultérieure sont prêtes, il nous en informe par son intermédiaire:

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

Le tableau d'octets outDate est un véritable trésor. Il contient des éléments prêts à l'emploi d'un flux vidéo H264 encodé avec lequel nous pouvons maintenant faire ce que nous voulons.

Les voici ...



Certaines pièces peuvent être trop volumineuses pour être transmises sur le réseau, mais rien, le système, si nécessaire, les découpera par lui-même et les enverra au destinataire.
Mais si c'est très effrayant, alors vous pouvez vous déchiqueter en poussant un tel fragment
  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; try { // Log.i(LOG_TAG, " outDate.length : " + ds.length); DatagramPacket packet = new DatagramPacket(ds, ds.length, address, port); udpSocket.send(packet); } catch (IOException e) { Log.i(LOG_TAG, "   UDP "); } } while (temp>=0); 


Mais pour l'instant, nous devons voir de première main que les données dans le tampon sont vraiment un flux vidéo H264. Par conséquent, envoyons-les dans un fichier:

Nous écrirons dans la configuration:

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

Et dans le rappel où est le tampon:

 try { outputStream.write(outDate, 0, outDate.length);//     } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } 

Ouvrez l'application, appuyez sur le bouton: "ALLUMER L'APPAREIL PHOTO ET LE FLUX". L'enregistrement démarre automatiquement. Nous attendons un peu et appuyons sur le bouton stop.

Le fichier enregistré ne sera normalement pas perdu, car le format n'est pas MP4, mais si vous l'ouvrez avec un lecteur VLC ou le convertissez en ligne en utilisant ONLINE CONVERT , nous nous assurerons que nous sommes sur la bonne voie. Certes, l'image se trouve sur le côté, mais elle est réparable.

En général, pour chaque événement d'enregistrement, de photographie ou de streaming, il est bien sûr préférable d'ouvrir une nouvelle session à chaque fois et de fermer l'ancienne. Autrement dit, nous allumons d'abord l'appareil photo et lançons l'aperçu nu. Ensuite, si vous devez prendre une photo, fermez l'aperçu et ouvrez l'aperçu, mais avec Image Reader fixé. Si nous passons à l'enregistrement vidéo, fermez la session en cours et démarrez la session avec l'aperçu et le Media Recorder qui y sont attachés. Je ne l'ai pas fait, pour que la visibilité du code n'en souffre pas, et vous décidez comment cela vous convient plus.

Et voici tout le code.

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


Et n'oubliez pas les autorisations dans le manifeste.

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

Nous nous sommes donc assurés que Media Codec fonctionne. Mais l'utiliser pour écrire une vidéo dans un fichier est en quelque sorte insensé. L'enregistreur multimédia peut gérer cette tâche beaucoup mieux et il ajoutera du son. Par conséquent, nous jetterons à nouveau la partie fichier et ajouterons un bloc de code pour la vidéo en streaming sur le réseau en utilisant le protocole udp. C'est aussi très simple.

Tout d'abord, nous initialisons le serveur UDP pratiquement.

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

Et dans le même rappel, où nous avons envoyé des données de préparation au flux pour le fichier, nous allons maintenant les envoyer sous forme de datagrammes à notre réseau domestique (j'espère que tout le monde les a?)

  try { DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port); udpSocket.send(packet); } catch (IOException e) { Log.i(LOG_TAG, "   UDP "); } 


C'est tout?

Il semblerait, mais non. L'application s'éclaircira au démarrage. Vous voyez, le système n'aime pas que dans le flux principal, nous envoyons toutes sortes de paquets de datagrammes. Mais il n'y a aucune raison de paniquer. Premièrement, bien que nous soyons dans le thread principal, nous travaillons toujours de manière asynchrone, c'est-à-dire pour déclencher un rappel. Deuxièmement, l'envoi de paquets udp est le même processus asynchrone. Nous disons seulement au système d'exploitation qu'il serait bien d'envoyer un paquet, mais que nous comptons entièrement sur lui dans cette affaire. Par conséquent, afin qu'Android ne se rebelle pas, nous ajouterons deux lignes au début du programme:

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

En général, le petit programme de démonstration élégant suivant se révélera:

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

Je ne sais pas comment font les autres, mais sur mon Red Note 7, vous pouvez même voir comment les kilo-octets sont téléchargés à la bonne adresse



Et il y a beaucoup de telles sockets udp, combien de bande passante réseau est suffisante. L'essentiel est qu'il y ait des adresses où. Vous aurez une émission.

Maintenant, allons chercher l'adresse souhaitée sur l'ordinateur


Je dois dire que tous les programmes informatiques ne sont pas capables d'absorber et de digérer un flux vidéo H264 via un seul canal udp sans aucune information supplémentaire. Mais certains le peuvent. Il s'agit par exemple du lecteur multimédia VLC extrêmement connu. C'est tellement cool que si vous commencez à décrire ses capacités, vous obtenez un livre entier dans l'article. Vous l'avez sûrement. Sinon, mettez-le.

Et à en juger par la description des commandes, les paquets udp peuvent digérer ce lecteur.

 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>]] 

Et toutes ces adresses source et adresse de liaison, en théorie, ne sont pas nécessaires. Seul le port d'écoute est nécessaire.



Et pourtant, bien sûr, vous ne devez pas oublier d'autoriser ce port à écouter (Malvar)



Saviez-vous que Windows ne vous permet pas de faire un écran d'impression à partir du moniteur de ressources?

Ou vous pouvez désactiver le pare-feu (je ne le recommande pas)

Donc, après avoir surmonté ces épines, nous lançons le lecteur VLC avec notre adresse et profitons de l'écran vide. Pas de vidéo.

Comment ça?


Et ainsi. Vous avez probablement la dernière version de VLC 3.08 Vetinari? Voilà, dans cette version d'udp, il est déclaré obsolète et, de plus, il est foutu.

La logique des développeurs du joueur est donc claire. Peu de gens ont besoin d'utiliser le canal udp nu aujourd'hui:

  • . , . .

Par conséquent, les gens normaux, bien sûr, utilisent des protocoles de niveau supérieur RTP et autres. Autrement dit, vous écrivez un serveur qui utilise udp (pour la vitesse) de toute façon, mais en même temps échange des informations de contrôle avec le client auquel il diffuse la vidéo. Quelle est sa bande passante, est-il nécessaire d'augmenter ou de diminuer le cache pour les données, quels détails d'image sont optimaux maintenant, et ainsi de suite. Encore une fois, le son est parfois nécessaire. Et il a besoin, vous savez, de synchronisation avec la vidéo.

Écoutez, les gars d'Odnoklassniki ont même dû déposer leur protocole pour le streaming. Mais leurs tâches, bien sûr, sont beaucoup plus importantes - envoyer des vidéos avec des chats à des dizaines de millions de femmes au foyer à travers le monde. Là, vous ne gérerez pas un canal udp.
Mais nous sommes en quelque sorte tristes d'écrire notre serveur RTP sur Android. Probablement, vous pouvez même trouver des éléments prêts à l'emploi et même gratuits, mais essayons de ne pas compliquer les entités pour l'instant. Prenez simplement la version du lecteur VLC où le streaming udp fonctionnait toujours.

Alors, téléchargez ici VLC 2.2.6 Umbrella

Install au lieu de ou à côté de l'ancien (c'est-à-dire le nouveau VLC), comme vous le souhaitez.

Nous commençons et voyons à nouveau un écran vide.

Et tout cela parce que nous n'avons évidemment pas configuré l'utilisation du codec H264. Ainsi, VLC serait en mesure de sélectionner automatiquement le codec s'il devait traiter le fichier (dans les paramètres initialement, la sélection automatique était spécifiée). Mais ils jettent un flux d'octets sur un seul canal, et il existe des dizaines de codecs pris en charge par VLC. Comment peut-il déterminer lequel appliquer?

Par conséquent, nous installons le codec de force.



Et maintenant, nous apprécions la diffusion de vidéo "en direct". La seule chose est que pour une raison quelconque, il se trouve sur le côté, mais cela est déjà facilement corrigé dans les paramètres du lecteur vidéo.

Et vous pouvez simplement démarrer le lecteur à partir de la ligne de commande avec cette clé:

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

Et il va se décoder et tourner.


Le streaming fonctionne donc. Il ne reste plus qu'à l'intégrer dans la fenêtre JAVA de l'application de contrôle du robot. Nous en traiterons très prochainement dans la dernière partie.

Source: https://habr.com/ru/post/fr473036/


All Articles