Android Camera2 API dari teko, bagian 3. Media Codec dan streaming video melalui UDP

Jadi, kami menemukan cara untuk mengambil foto dan merekam video menggunakan Camera2 API. Tetap hanya mempelajari cara mentransfer aliran video dari perangkat Android ke penerima yang menderita dari luar. Tujuan utamanya, seperti yang telah berulang kali dikatakan sebelumnya, adalah intelektualisasi robot - kami menempatkan smartphone di atasnya dan, dengan kata lain, mengubah monyet menjadi seseorang. Media Codec akan membantu kami dalam hal ini. Dan tentu saja, Camera2 API baru.


Siapa yang peduli, tolong, di bawah kucing.

Detail tentang proyek robot dapat ditemukan di sini , tetapi untuk saat ini , kami akan langsung mengalirkan video darinya (atau lebih tepatnya, dari smartphone Android yang terpasang padanya) ke komputer elektronik pribadi.

Apa yang kita butuhkan untuk ini?


Untuk mentransfer aliran video dari layar smartphone ke tempat lain, seperti yang Anda tahu, itu (stream) pertama-tama harus dikonversi ke format menyusut yang sesuai (akan terlalu tebal untuk mengirimkan bingkai demi bingkai), meletakkan perangko waktu (perangko waktu) dan mengirimkan dalam bentuk biner kepada penerima. . Yang akan melakukan operasi decoding terbalik.

Justru perbuatan hitam tingkat rendah inilah yang telah ditangani oleh kelas Media Codec sejak 2013, sejak rilis Android 4.3.



Hal lain adalah bahwa mendekati pengkodean video, tidak seperti hari ini, tidak begitu sederhana. Untuk mendapatkan gambar dari kamera, perlu menggunakan banyak kode misterius di mana, seperti dalam mantra dukun Yakut, satu-satunya ketidaktepatan dapat menyebabkan crash lengkap aplikasi. Tambahkan ke ini API Kamera sebelumnya, di mana alih-alih callback yang sudah jadi, Anda harus menulis sendiri pena yang disinkronkan, dan kegiatan ini, katakanlah, bukan untuk orang yang lemah hati.

Dan yang paling penting, Anda melihat kode kerja dari jauh, semuanya tampak jelas secara umum. Anda mulai mentransfer sebagian ke proyek Anda - tidak jelas mengapa itu mengalir. Tetapi tidak mungkin untuk dikoreksi, karena sulit untuk memahami detailnya.

Ya, dan dari solid entah bagaimana merasa nyaman. Singkatnya, berantakan

Untungnya, untuk orang yang berpikiran lambat, pembangun Google telah memperkenalkan konsep ajaib Surface , yang dapat digunakan untuk menghindari detail tingkat rendah. Sulit bagi saya sebagai orang awam untuk memahami berapa biayanya dan apa yang hilang dari pengembang, tetapi sekarang kita hampir dapat mengatakan: "Android, bawa Permukaan ini ke mana video dari kamera ditampilkan dan tidak mengubah apa pun di sana, baik, seperti, encode dan kirim. " Dan hal yang paling menakjubkan adalah ia bekerja. Dan dengan Camera2 API baru, program itu sendiri tahu kapan mengirim data, panggilan balik baru telah muncul!

Jadi sekarang untuk menyandikan video - ludah saja. Apa yang akan kita lakukan sekarang.
Kami mengambil kode dari artikel pertama dan, seperti biasa, membuang semuanya kecuali tombol dan inisialisasi kamera.

Mari kita mulai dengan Layout untuk aplikasi.
<?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> 


Dan akhiri dengan Media Codec hooking


Dalam posting terakhir, kami menampilkan gambar dari kamera pada Permukaan dan menulis video darinya menggunakan MediaRecorder. Untuk melakukan ini, kami cukup menentukan kedua komponen dalam daftar Permukaan.

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

Di sini hal yang sama, hanya alih-alih mMediaRecorder kami tentukan:

 (Arrays.asList(surface, mEncoderSurface), 

Ternyata, kira-kira seperti:

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

Apa itu mEncoderSurface? Dan ini adalah Permukaan yang sama yang akan bekerja dengan Media Codec. Untuk memulai, Anda perlu menginisialisasi keduanya dengan cara ini.

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

Sekarang tinggal mendaftarkan satu panggilan balik. Ketika Media Codec tiba-tiba merasa bahwa data berikutnya untuk penyiaran lebih lanjut siap, dia akan memberi tahu kami tentang hal itu melalui dia:

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

Array byte outDate adalah harta nyata. Ini berisi potongan-potongan yang sudah jadi dari aliran video H264 yang dikodekan yang dengannya kita sekarang dapat melakukan apa pun yang kita inginkan.

Di sini mereka ...



Beberapa potongan mungkin terlalu besar untuk ditransmisikan melalui jaringan, tetapi tidak ada, sistem, jika perlu, akan memotongnya dengan sendirinya dan mengirimkannya ke penerima.
Tetapi jika itu sangat menakutkan, maka Anda dapat merusak diri sendiri dengan mendorong fragmen tersebut
  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); 


Tetapi untuk sekarang, kita perlu melihat secara langsung bahwa data dalam buffer sebenarnya adalah aliran video H264. Karena itu, mari kita kirim mereka ke file:

Kami akan menulis di setup:

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

Dan di dalam callback di mana buffer:

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

Buka aplikasi, tekan tombol: "TURN ON THE CAMERA AND STREAM". Perekaman dimulai secara otomatis. Kami menunggu sebentar dan menekan tombol stop.

File yang disimpan biasanya mungkin tidak akan hilang, karena formatnya bukan MP4, tetapi jika Anda membukanya dengan pemutar VLC atau mengonversinya secara online menggunakan CONVERT ONLINE , kami akan memastikan bahwa kami berada di jalur yang benar. Benar, gambar terletak pada sisinya, tetapi dapat diperbaiki.

Secara umum, untuk setiap acara perekaman, pemotretan atau streaming, lebih baik, tentu saja, untuk membuka sesi baru setiap kali, dan untuk menutup yang lama. Artinya, pertama-tama kita nyalakan kamera dan meluncurkan pratinjau telanjang. Kemudian, jika Anda perlu mengambil gambar, tutup pratinjau dan buka pratinjau, tetapi dengan Image Reader diikat. Jika kita beralih ke perekaman video, maka tutup sesi saat ini dan mulai sesi dengan pratinjau dan Perekam Media terlampir. Saya tidak melakukan ini, sehingga visibilitas kode tidak menderita, dan Anda memutuskan bagaimana itu lebih nyaman untuk diri sendiri.

Dan di sini adalah seluruh kode.

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


Dan jangan lupa tentang izin dalam manifes.

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

Jadi, kami memastikan bahwa Media Codec berfungsi. Tetapi menggunakannya untuk menulis video ke suatu file entah bagaimana tidak memiliki semangat. Media Recorder dapat menangani tugas ini dengan lebih baik, dan itu akan menambah suara. Oleh karena itu, kami akan membuang bagian file lagi dan menambahkan blok kode untuk streaming video ke jaringan menggunakan protokol udp. Ini juga sangat sederhana.

Pertama, kami menginisialisasi server UDP secara praktis.

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

Dan dalam panggilan balik yang sama, di mana kami mengirim data kesiapan ke aliran untuk file, sekarang kami akan mengirimkannya dalam bentuk datagram ke jaringan rumah kami (saya harap semua orang memilikinya?)

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


Hanya itu semua

Tampaknya, tetapi tidak. Aplikasi akan cerah saat startup. Anda lihat, sistem tidak seperti itu di aliran utama kami mengirim semua jenis paket datagram. Tapi tidak ada alasan untuk panik. Pertama, meskipun kami berada di utas utama, kami masih bekerja secara tidak sinkron, yaitu untuk memicu panggilan balik. Yang kedua mengirim paket udp adalah proses asinkron yang sama. Kami hanya memberi tahu sistem operasi bahwa akan lebih baik untuk mengirim paket, tetapi kami sepenuhnya mengandalkannya dalam hal ini. Karena itu, agar Android tidak memberontak, kami akan menambahkan dua baris di awal program:

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

Secara umum, program demo kecil elegan berikut akan berubah:

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

Saya tidak tahu bagaimana orang lain melakukannya, tetapi pada Red Note 7 saya, Anda bahkan dapat melihat bagaimana kilobyte diunduh di alamat yang benar



Dan ada banyak soket udp tersebut, berapa banyak bandwidth jaringan yang cukup. Yang utama ada alamatnya dimana. Anda akan memiliki siaran.

Sekarang mari kita pergi mencari alamat yang diinginkan di komputer


Saya harus mengatakan bahwa tidak setiap program komputer mampu menyerap dan mencerna aliran video H264 melalui saluran udp tunggal tanpa informasi tambahan. Tetapi beberapa mungkin. Ini misalnya pemutar media VLC yang sangat terkenal. Ini adalah hal yang sangat keren sehingga jika Anda mulai mendeskripsikan kemampuannya, maka dari artikel tersebut Anda mendapatkan seluruh buku. Tentunya Anda memilikinya. Jika tidak, kenakan.

Dan dilihat dari deskripsi perintah untuk itu, paket udp dapat mencerna pemain ini.

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

Dan semua alamat sumber dan alamat mengikat ini, secara teori, tidak diperlukan. Hanya port mendengarkan yang diperlukan.



Namun, tentu saja, Anda tidak boleh lupa tentang mengizinkan port ini untuk mendengarkan (Malvar)



Tahukah Anda bahwa Windows tidak memungkinkan Anda membuat layar cetak dari monitor sumber daya?

Atau Anda dapat menonaktifkan firewall sama sekali (saya tidak merekomendasikannya)

.Jadi, setelah mengatasi duri-duri ini, kami meluncurkan pemutar VLC dengan alamat kami dan menikmati layar kosong. Tidak ada video

Bagaimana bisa begitu?


Dan begitulah. Anda mungkin memiliki versi terbaru VLC 3.08 Vetinari? Itu saja, dalam versi udp ini, itu dinyatakan usang dan, apalagi, itu kacau.

Jadi logika pengembang pemain jelas. Hanya sedikit orang yang perlu menggunakan saluran udp kosong saat ini karena:

  • . , . .

Oleh karena itu, orang normal, tentu saja, menggunakan protokol tingkat tinggi RTP dan lainnya. Yaitu, di jari - Anda menulis server yang menggunakan udp (untuk kecepatan), tetapi pada saat yang sama bertukar informasi kontrol dengan klien kepada siapa itu streaming video. Apa bandwidth-nya, apakah perlu untuk menambah atau mengurangi cache untuk data, detail gambar apa yang optimal sekarang, dan seterusnya dan seterusnya. Sekali lagi, suara juga kadang dibutuhkan. Dan dia perlu, Anda tahu, sinkronisasi dengan video.

Lihat, orang-orang dari Odnoklassniki bahkan harus mengajukan protokol mereka untuk streaming. Tapi tugas mereka, tentu saja, jauh lebih penting - mengirim video dengan kucing ke puluhan juta ibu rumah tangga di seluruh dunia. Di sana Anda tidak akan mengelola satu saluran udp.
Tapi kami agak sedih menulis server RTP kami di android. Mungkin, Anda bahkan dapat menemukan siap pakai dan bahkan gratis, tetapi mari kita coba untuk tidak menyulitkan entitas untuk saat ini. Ambil saja versi VLC player di mana udp streaming masih berfungsi.

Jadi, unduh dari sini VLC 2.2.6

Instal Payung alih-alih atau di sebelah yang lama (yaitu, VLC baru), seperti yang Anda inginkan.

Kami memulai dan melihat layar kosong lagi.

Dan semua ini karena kami jelas tidak mengkonfigurasi penggunaan codec H264. Jadi VLC akan dapat memilih codec secara otomatis jika harus berurusan dengan file (pada pengaturan awalnya, pemilihan otomatis ditentukan). Tetapi mereka membuang aliran byte ke saluran tunggal, dan ada puluhan codec yang didukung VLC. Bagaimana dia bisa tahu yang mana yang harus diterapkan?

Karena itu, kami memasang codec dengan paksa.



Dan sekarang kami menikmati siaran video "langsung". Satu-satunya hal adalah karena beberapa alasan ia ada di sisinya, tetapi ini sudah mudah diperbaiki dalam pengaturan pemutar video.

Dan Anda bisa mulai pemain dari baris perintah dengan kunci ini:

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

Dan itu akan memecahkan kode itu sendiri dan berbalik.


Jadi streaming berfungsi. Tetap hanya untuk mengintegrasikannya ke dalam jendela JAVA dari aplikasi kontrol robot. Kami akan menangani ini segera di bagian terakhir.

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


All Articles