Android Camera2 API من إبريق الشاي ، الجزء 3. ترميز الوسائط ودفق الفيديو عبر UDP

لذلك ، اكتشفنا نوعًا ما كيفية التقاط الصور وتسجيل مقاطع الفيديو باستخدام Camera2 API. يبقى فقط لمعرفة كيفية نقل دفق الفيديو من جهاز Android إلى المستلمين الذين يعانون من الخارج. الهدف النهائي ، كما قيل مرارًا وتكرارًا ، هو تفكير الروبوتات - وضعنا هاتفًا ذكيًا عليه ، وإذا جاز التعبير ، فقم بتحويل القرد إلى شخص. سوف يساعدك برنامج ترميز الوسائط في هذا. وبالطبع ، واجهة برمجة تطبيقات Camera2 الجديدة.


من يهتم ، من فضلك ، تحت القط.

يمكن العثور هنا على تفاصيل حول المشروع الروبوتي ، ولكن في الوقت الحالي ، سننقل الفيديو منه مباشرةً (أو بالأحرى ، من هاتف ذكي يعمل بنظام Android متصل به) إلى جهاز كمبيوتر شخصي إلكتروني.

ماذا نحتاج لهذا؟


من أجل نقل دفق فيديو من شاشة الهاتف الذكي إلى مكان آخر ، كما تعلم ، يجب أولاً تحويله (دفق) إلى تنسيق مناسب تقلصت (سيكون حجمه سميكًا جدًا بحيث لا يمكن إرسال الإطار حسب الإطار) ، ووضع طوابع زمنية (طوابع زمنية) وإرسالها في شكل ثنائي إلى المستلم . والتي سوف تؤدي عملية فك التشفير العكسي.

هذه هي الأفعال السوداء المنخفضة المستوى التي تتعامل معها فئة Media Codec منذ عام 2013 ، بدءًا من تاريخ إصدار Android 4.3.



شيء آخر هو أن الاقتراب المبكر من تشفير الفيديو ، على عكس اليوم ، لم يكن بهذه البساطة. للحصول على صورة من الكاميرا ، كان من الضروري استخدام أطنان من الشفرة الغامضة التي ، كما هو الحال في تعويذات شامان Yakut ، يمكن أن يؤدي عدم الدقة الوحيد إلى تعطل التطبيق بالكامل. أضف إلى ذلك واجهة برمجة التطبيقات السابقة للكاميرا ، حيث بدلاً من عمليات معاودة الاتصال الجاهزة ، كان عليك أن تكتب الأقلام المتزامنة المختلفة بنفسك ، وهذا النشاط ، ولنقل ، ليس لضعاف القلب.

والأهم من ذلك ، أنظر إلى مدونة العمل من بعيد ، ويبدو أن كل شيء واضح بعبارات عامة. تبدأ في النقل في أجزاء إلى مشروعك - ليس من الواضح سبب سكبه. لكن من المستحيل تصحيحه ، لأنه من الصعب فهم التفاصيل.

نعم ، ومن الصلبة إهمال بطريقة أو بأخرى في سهولة. باختصار ، فوضى

لحسن الحظ ، بالنسبة لبطئ البطلين ، قدّم بناة Google المفهوم السحري لـ Surface ، والذي يمكنك من خلاله تجنب التفاصيل ذات المستوى المنخفض. من الصعب بالنسبة لي كرجل عادي أن أفهم بأي ثمن وما الذي يخسره المطور ، ولكن الآن يمكننا أن نقول حرفيًا: "أندرويد ، خذ هذا السطح الذي يظهر عليه الفيديو من الكاميرا ولا يغير شيئًا هناك ، كما هو ، يتم ترميزه وإرسال على ". والشيء الأكثر مذهلة هو أنه يعمل. ومع Camera2 API الجديد ، يعرف البرنامج نفسه متى يتم إرسال البيانات ، وقد ظهرت عمليات رد اتصال جديدة!

حتى الآن لترميز الفيديو - بصق فقط. ماذا سنفعل الآن.
نأخذ الكود من المقالة الأولى ، وكالعادة ، نلقي كل شيء منه باستثناء الأزرار وتهيئة الكاميرا.

لنبدأ بالتخطيط للتطبيق.
<?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> 


وينتهي مع تثبيت برنامج ترميز الوسائط


في المنشور الأخير ، عرضنا الصورة من الكاميرا على السطح وكتبنا الفيديو باستخدام MediaRecorder. للقيام بذلك ، حددنا ببساطة كلا المكونين في قائمة السطح.

 (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 الذي سيعمل به برنامج ترميز الوسائط. فقط للبدء ، تحتاج إلى تهيئة كل منهما بهذه الطريقة تقريبًا.

  private void setUpMediaCodec() { try { mCodec = MediaCodec.createEncoderByType("video/avc"); // H264  } catch (Exception e) { Log.i(LOG_TAG, "  "); } int width = 320; //   int height = 240; //   int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; //    int videoBitrate = 500000; //    bps (  ) int videoFramePerSecond = 20; // FPS int iframeInterval = 2; // I-Frame    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval); mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //     mEncoderSurface = mCodec.createInputSurface(); //  Surface  mCodec.setCallback(new EncoderCallback()); mCodec.start(); //   Log.i(LOG_TAG, " "); } 

الآن يبقى تسجيل رد اتصال واحد. عندما يشعر Media Codec فجأة أن البيانات التالية للبث الإضافي جاهزة ، فسوف يخطرنا بذلك من خلاله:

  private class EncoderCallback extends MediaCodec.Callback { @Override public void onInputBufferAvailable(MediaCodec codec, int index) { } @Override public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { outPutByteBuffer = mCodec.getOutputBuffer(index); byte[] outDate = new byte[info.size]; outPutByteBuffer.get(outDate); Log.i(LOG_TAG, " outDate.length : " + outDate.length); mCodec.releaseOutputBuffer(index, false); } @Override public void onError(MediaCodec codec, MediaCodec.CodecException e) { Log.i(LOG_TAG, "Error: " + e); } @Override public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { Log.i(LOG_TAG, "encoder output format changed: " + format); } } 

مجموعة البايت outDate هي كنز حقيقي. أنه يحتوي على قطع جاهزة من دفق الفيديو H264 المشفرة والتي يمكننا الآن القيام بكل ما نريد.

ها هم ...



قد تكون بعض القطع كبيرة جدًا بحيث لا يمكن نقلها عبر الشبكة ، ولكن لا شيء ، سيقوم النظام ، إذا لزم الأمر ، بقطعها بنفسه وإرسالها إلى المستلم.
ولكن إذا كان الأمر مخيفًا للغاية ، فيمكنك أن تمزّق نفسك عن طريق تحطيم هذه القطعة
  int count =0; int temp =outDate.length ; do {//    byte[] ds; temp = temp-1024; if(temp>=0) { ds = new byte[1024];} else { ds = new byte[temp+1024];} for(int i =0;i<ds.length;i++) { ds[i]=outDate[i+1024*count]; } count=count+1; 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); 


ولكن في الوقت الحالي ، نحتاج إلى أن نرى بشكل مباشر أن البيانات الموجودة في المخزن المؤقت هي في الواقع دفق فيديو H264. لذلك ، دعنا نرسلهم إلى ملف:

سنكتب في الإعداد:

  private void setUpMediaCodec() { File mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test3.h264"); try { outputStream = new BufferedOutputStream(new FileOutputStream(mFile)); Log.i("Encoder", "outputStream initialized"); } catch (Exception e) { e.printStackTrace(); } 

وفي رد الاتصال حيث يوجد المخزن المؤقت:

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

افتح التطبيق ، اضغط على الزر: "اقلب على الكاميرا والبث". يبدأ التسجيل تلقائيًا. ننتظر قليلا واضغط على زر التوقف.

عادةً لن يتم فقد الملف المحفوظ ، نظرًا لأن التنسيق ليس MP4 ، ولكن إذا فتحته باستخدام مشغل VLC أو قمت بتحويله عبر الإنترنت باستخدام ONLINE CONVERT ، فسنتأكد من أننا على المسار الصحيح. صحيح أن الصورة تقع على جانبها ، لكنها قابلة للتثبيت.

بشكل عام ، لكل حدث من التسجيل أو التصوير أو البث ، من الأفضل ، بالطبع ، فتح جلسة جديدة في كل مرة ، وإغلاق الجلسة القديمة. وهذا هو ، أولاً تشغيل الكاميرا وإطلاق معاينة عارية. ثم ، إذا كنت بحاجة إلى التقاط صورة ، فأغلق المعاينة وافتح المعاينة ، ولكن مع تثبيت Image Reader. إذا تحولنا إلى تسجيل الفيديو ، فقم بإغلاق الجلسة الحالية وبدء الجلسة مع المعاينة ومسجل الوسائط المرفق بها. لم أفعل هذا ، حتى لا تتأثر رؤية الرمز ، وتقرر كيف يكون أكثر ملاءمة لنفسك.

وهنا هو الكود كله.

BasicMediaCodec
 package com.example.basicmediacodec; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.StrictMode; import android.util.Log; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.Button; import android.widget.Toast; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; public class MainActivity extends AppCompatActivity { public static final String LOG_TAG = "myLogs"; public static Surface surface = null; CameraService[] myCameras = null; private CameraManager mCameraManager = null; private final int CAMERA1 = 0; private Button mButtonOpenCamera1 = null; private Button mButtonStreamVideo = null; private Button mButtonTStopStreamVideo = null; public static TextureView mImageView = null; private HandlerThread mBackgroundThread; private Handler mBackgroundHandler = null; private MediaCodec mCodec = null; //  Surface mEncoderSurface; // Surface      BufferedOutputStream outputStream; ByteBuffer outPutByteBuffer; private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } private void stopBackgroundThread() { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { e.printStackTrace(); } } @RequiresApi(api = Build.VERSION_CODES.M) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setContentView(R.layout.activity_main); Log.d(LOG_TAG, " "); if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) ) { requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } mButtonOpenCamera1 = findViewById(R.id.button1); mButtonStreamVideo = findViewById(R.id.button2); mButtonTStopStreamVideo = findViewById(R.id.button3); mImageView = findViewById(R.id.textureView); mButtonOpenCamera1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setUpMediaCodec();//    if (myCameras[CAMERA1] != null) {//   if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera(); } } }); mButtonStreamVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { { //    } } }); mButtonTStopStreamVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mCodec != null) { Toast.makeText(MainActivity.this, "  ", Toast.LENGTH_SHORT).show(); myCameras[CAMERA1].stopStreamingVideo(); } } }); mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { //      myCameras = new CameraService[mCameraManager.getCameraIdList().length]; for (String cameraID : mCameraManager.getCameraIdList()) { Log.i(LOG_TAG, "cameraID: " + cameraID); int id = Integer.parseInt(cameraID); //     myCameras[id] = new CameraService(mCameraManager, cameraID); } } catch (CameraAccessException e) { Log.e(LOG_TAG, e.getMessage()); e.printStackTrace(); } } public class CameraService { private String mCameraID; private CameraDevice mCameraDevice = null; private CameraCaptureSession mSession; private CaptureRequest.Builder mPreviewBuilder; public CameraService(CameraManager cameraManager, String cameraID) { mCameraManager = cameraManager; mCameraID = cameraID; } private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; Log.i(LOG_TAG, "Open camera with id:" + mCameraDevice.getId()); startCameraPreviewSession(); } @Override public void onDisconnected(CameraDevice camera) { mCameraDevice.close(); Log.i(LOG_TAG, "disconnect camera with id:" + mCameraDevice.getId()); mCameraDevice = null; } @Override public void onError(CameraDevice camera, int error) { Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error); } }; private void startCameraPreviewSession() { SurfaceTexture texture = mImageView.getSurfaceTexture(); texture.setDefaultBufferSize(320, 240); surface = new Surface(texture); try { mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewBuilder.addTarget(surface); mPreviewBuilder.addTarget(mEncoderSurface); mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { mSession = session; try { mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession session) { } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } public boolean isOpen() { if (mCameraDevice == null) { return false; } else { return true; } } public void openCamera() { try { if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler); } } catch (CameraAccessException e) { Log.i(LOG_TAG, e.getMessage()); } } public void closeCamera() { if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } public void stopStreamingVideo() { if (mCameraDevice != null & mCodec != null) { try { mSession.stopRepeating(); mSession.abortCaptures(); } catch (CameraAccessException e) { e.printStackTrace(); } mCodec.stop(); mCodec.release(); mEncoderSurface.release(); closeCamera(); } } } private void setUpMediaCodec() { File mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "test3.h264"); try { outputStream = new BufferedOutputStream(new FileOutputStream(mFile)); Log.i("Encoder", "outputStream initialized"); } catch (Exception e) { e.printStackTrace(); } try { mCodec = MediaCodec.createEncoderByType("video/avc"); // H264  } catch (Exception e) { Log.i(LOG_TAG, "  "); } int width = 320; //   int height = 240; //   int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; //    int videoBitrate = 500000; //    bps (  ) int videoFramePerSecond = 20; // FPS int iframeInterval = 3; // I-Frame    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval); mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //     mEncoderSurface = mCodec.createInputSurface(); //  Surface  mCodec.setCallback(new EncoderCallback()); mCodec.start(); //   Log.i(LOG_TAG, " "); } private class EncoderCallback extends MediaCodec.Callback { @Override public void onInputBufferAvailable(MediaCodec codec, int index) { } @Override public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { outPutByteBuffer = mCodec.getOutputBuffer(index); byte[] outDate = new byte[info.size]; outPutByteBuffer.get(outDate); try { Log.i(LOG_TAG, " outDate.length : " + outDate.length); outputStream.write(outDate, 0, outDate.length);//     } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } mCodec.releaseOutputBuffer(index, false); } @Override public void onError(MediaCodec codec, MediaCodec.CodecException e) { Log.i(LOG_TAG, "Error: " + e); } @Override public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { Log.i(LOG_TAG, "encoder output format changed: " + format); } } @Override public void onPause() { if (myCameras[CAMERA1].isOpen()) { myCameras[CAMERA1].closeCamera(); } stopBackgroundThread(); super.onPause(); } @Override public void onResume() { super.onResume(); startBackgroundThread(); } } 


ولا تنس الأذونات في البيان.

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

لذلك ، تأكدنا من أن برنامج Media Codec يعمل. لكن استخدامه لكتابة الفيديو إلى ملف هو بلا روح. يمكن لمسجل الوسائط التعامل مع هذه المهمة بشكل أفضل ، وسيضيف صوتًا. لذلك ، سنطرح جزء الملف مرة أخرى ونضيف كتلة تعليمات برمجية لبث الفيديو إلى الشبكة باستخدام بروتوكول udp. إنها أيضًا بسيطة جدًا.

أولاً ، نهيئ خادم UDP بشكل عملي.

  DatagramSocket udpSocket; String ip_address = "192.168.1.84"; //      InetAddress address; int port = 40002; // ,      …….. try { udpSocket = new DatagramSocket(); Log.i(LOG_TAG, "  udp "); } catch ( SocketException e) { Log.i(LOG_TAG, "   udp "); } try { address = InetAddress.getByName(ip_address); Log.i(LOG_TAG, "  "); } catch (Exception e) { 

وفي نفس رد الاتصال ، حيث أرسلنا بيانات الاستعداد إلى الدفق للملف ، سنرسلها الآن في شكل مخططات بيانات إلى شبكتنا المنزلية (آمل أن يكون لدى الجميع؟)

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


هل هذا كل شيء؟

يبدو ، ولكن لا. سوف التطبيق سطع عند بدء التشغيل. كما ترى ، لا يحب النظام في الدفق الرئيسي إرسال جميع أنواع حزم مخططات البيانات. ولكن ليس هناك سبب للهلع. أولاً ، على الرغم من أننا في الخيط الرئيسي ، ما زلنا نعمل بشكل غير متزامن ، أي لبدء رد الاتصال. ثانياً ، إرسال حزم udp هو نفس العملية غير المتزامنة. إننا نعلم نظام التشغيل فقط أنه سيكون من الجيد إرسال حزمة ، لكننا نعتمد عليها تمامًا في هذا الشأن. لذلك ، حتى لا يقوم Android بالتمرد ، سنضيف سطرين في بداية البرنامج:

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

بشكل عام ، سينتهي البرنامج التجريبي الأنيق الصغير التالي:

 package com.example.basicmediacodec; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.StrictMode; import android.util.Log; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.Button; import android.widget.Toast; import java.io.BufferedOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.nio.ByteBuffer; import java.util.Arrays; public class MainActivity extends AppCompatActivity { public static final String LOG_TAG = "myLogs"; public static Surface surface = null; CameraService[] myCameras = null; private CameraManager mCameraManager = null; private final int CAMERA1 = 0; private Button mButtonOpenCamera1 = null; private Button mButtonStreamVideo = null; private Button mButtonTStopStreamVideo = null; public static TextureView mImageView = null; private HandlerThread mBackgroundThread; private Handler mBackgroundHandler = null; private MediaCodec mCodec = null; //  Surface mEncoderSurface; // Surface      BufferedOutputStream outputStream; ByteBuffer outPutByteBuffer; DatagramSocket udpSocket; String ip_address = "192.168.1.84"; InetAddress address; int port = 40002; private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } private void stopBackgroundThread() { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { e.printStackTrace(); } } @RequiresApi(api = Build.VERSION_CODES.M) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setContentView(R.layout.activity_main); Log.d(LOG_TAG, " "); if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) ) { requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } mButtonOpenCamera1 = findViewById(R.id.button1); mButtonStreamVideo = findViewById(R.id.button2); mButtonTStopStreamVideo = findViewById(R.id.button3); mImageView = findViewById(R.id.textureView); mButtonOpenCamera1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setUpMediaCodec();//    if (myCameras[CAMERA1] != null) {//   if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera(); } } }); mButtonStreamVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { { //    } } }); mButtonTStopStreamVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mCodec != null) { Toast.makeText(MainActivity.this, "  ", Toast.LENGTH_SHORT).show(); myCameras[CAMERA1].stopStreamingVideo(); } } }); try { udpSocket = new DatagramSocket(); Log.i(LOG_TAG, "  udp "); } catch ( SocketException e) { Log.i(LOG_TAG, "   udp "); } try { address = InetAddress.getByName(ip_address); Log.i(LOG_TAG, "  "); } catch (Exception e) { } mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { //      myCameras = new CameraService[mCameraManager.getCameraIdList().length]; for (String cameraID : mCameraManager.getCameraIdList()) { Log.i(LOG_TAG, "cameraID: " + cameraID); int id = Integer.parseInt(cameraID); //     myCameras[id] = new CameraService(mCameraManager, cameraID); } } catch (CameraAccessException e) { Log.e(LOG_TAG, e.getMessage()); e.printStackTrace(); } } public class CameraService { private String mCameraID; private CameraDevice mCameraDevice = null; private CameraCaptureSession mSession; private CaptureRequest.Builder mPreviewBuilder; public CameraService(CameraManager cameraManager, String cameraID) { mCameraManager = cameraManager; mCameraID = cameraID; } private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; Log.i(LOG_TAG, "Open camera with id:" + mCameraDevice.getId()); startCameraPreviewSession(); } @Override public void onDisconnected(CameraDevice camera) { mCameraDevice.close(); Log.i(LOG_TAG, "disconnect camera with id:" + mCameraDevice.getId()); mCameraDevice = null; } @Override public void onError(CameraDevice camera, int error) { Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error); } }; private void startCameraPreviewSession() { SurfaceTexture texture = mImageView.getSurfaceTexture(); texture.setDefaultBufferSize(320, 240); surface = new Surface(texture); try { mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewBuilder.addTarget(surface); mPreviewBuilder.addTarget(mEncoderSurface); mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { mSession = session; try { mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession session) { } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } public boolean isOpen() { if (mCameraDevice == null) { return false; } else { return true; } } public void openCamera() { try { if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler); } } catch (CameraAccessException e) { Log.i(LOG_TAG, e.getMessage()); } } public void closeCamera() { if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } public void stopStreamingVideo() { if (mCameraDevice != null & mCodec != null) { try { mSession.stopRepeating(); mSession.abortCaptures(); } catch (CameraAccessException e) { e.printStackTrace(); } mCodec.stop(); mCodec.release(); mEncoderSurface.release(); closeCamera(); } } } private void setUpMediaCodec() { try { mCodec = MediaCodec.createEncoderByType("video/avc"); // H264  } catch (Exception e) { Log.i(LOG_TAG, "  "); } int width = 320; //   int height = 240; //   int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; //    int videoBitrate = 500000; //    bps (  ) int videoFramePerSecond = 20; // FPS int iframeInterval = 3; // I-Frame    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval); mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //     mEncoderSurface = mCodec.createInputSurface(); //  Surface  mCodec.setCallback(new EncoderCallback()); mCodec.start(); //   Log.i(LOG_TAG, " "); } private class EncoderCallback extends MediaCodec.Callback { @Override public void onInputBufferAvailable(MediaCodec codec, int index) { } @Override public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { outPutByteBuffer = mCodec.getOutputBuffer(index); byte[] outDate = new byte[info.size]; outPutByteBuffer.get(outDate); try { DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port); udpSocket.send(packet); } catch (IOException e) { Log.i(LOG_TAG, "   UDP "); } mCodec.releaseOutputBuffer(index, false); } @Override public void onError(MediaCodec codec, MediaCodec.CodecException e) { Log.i(LOG_TAG, "Error: " + e); } @Override public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { Log.i(LOG_TAG, "encoder output format changed: " + format); } } @Override public void onPause() { if (myCameras[CAMERA1].isOpen()) { myCameras[CAMERA1].closeCamera(); } stopBackgroundThread(); super.onPause(); } @Override public void onResume() { super.onResume(); startBackgroundThread(); } } 

لا أعرف كيف يفعل الآخرون ، لكن في ملاحظتي Red Note 7 ، يمكنك حتى معرفة كيفية تنزيل الكيلوبايت على العنوان الصحيح



وهناك الكثير من مآخذ udp ، مقدار عرض النطاق الترددي للشبكة يكفي. الشيء الرئيسي هو أن هناك عناوين حيث. سيكون لديك بث.

الآن دعنا نذهب للبحث عن العنوان المطلوب على الكمبيوتر


يجب أن أقول أنه ليس كل برنامج كمبيوتر قادر على استيعاب وهضم دفق الفيديو H264 عبر قناة UDP واحدة دون أي معلومات إضافية. لكن البعض قد. هذا على سبيل المثال مشغل الوسائط VLC المشهور جدًا. هذا شيء رائع ، إذا بدأت في وصف إمكانياتها ، فحينها تحصل على كتاب كامل من المقال. بالتأكيد لديك. إذا لم يكن كذلك ، ضعها على.

واستنادا إلى وصف الأوامر لذلك ، يمكن للحزم udp هضم هذا اللاعب.

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

وكل هذه العناوين المصدر وعنوان الربط ، من الناحية النظرية ، ليست هناك حاجة. مطلوب فقط منفذ الاستماع.



ومع ذلك ، بالطبع ، يجب ألا تنسى السماح لهذا المنفذ بالاستماع (Malvar)



هل تعلم أن Windows لا يسمح لك بإنشاء شاشة طباعة من شاشة مراقبة الموارد؟

أو يمكنك تعطيل جدار الحماية على الإطلاق (لا أوصي به)

لذلك ، بعد التغلب على هذه الأشواك ، نقوم بتشغيل مشغل VLC بعنواننا والاستمتاع بالشاشة الفارغة. لا يوجد فيديو

كيف ذلك؟


و كذلك. ربما لديك أحدث إصدار من VLC 3.08 Vetinari؟ هذا كل شيء ، في هذا الإصدار من UDP ، تم إهماله ، علاوة على ذلك ، كان في حالة سكر.

لذلك فإن منطق مطوري اللاعب واضح. قليل من الناس بحاجة إلى استخدام قناة udp العارية في الوقت الحاضر بسبب:

  • . , . .

لذلك ، الأشخاص العاديين ، بالطبع ، يستخدمون بروتوكولات عالية المستوى RTP وغيرها. أي على الأصابع - تكتب خادمًا يستخدم udp (للسرعة) على أي حال ، ولكن في نفس الوقت يتبادل معلومات التحكم مع العميل الذي يقوم ببث الفيديو إليه. ما هو النطاق الترددي ، هل من الضروري زيادة أو تقليل ذاكرة التخزين المؤقت للبيانات ، ما هي تفاصيل الصورة المثلى الآن ، وهكذا دواليك وما إلى ذلك. مرة أخرى ، هناك حاجة أيضا الصوت في بعض الأحيان. ويحتاج ، كما تعلمون ، إلى التزامن مع الفيديو.

انظر ، كان على اللاعبين من Odnoklassniki تقديم بروتوكولهم من أجل البث. لكن مهامهم ، بطبيعة الحال ، أكثر أهمية بكثير - لإرسال مقاطع فيديو مع القطط لعشرات الملايين من ربات البيوت في جميع أنحاء العالم. هناك لن تدير قناة UDP واحدة.
لكننا نحزن بطريقة أو بأخرى لكتابة خادم RTP على نظام Android. ربما ، يمكنك أن تجد حتى جاهزة ومجانية ، ولكن دعونا نحاول عدم تعقيد الكيانات في الوقت الحالي. ما عليك سوى استخدام إصدار مشغل VLC حيث لا يزال تدفق البث عبر بروتوكول الإنترنت يعمل.

لذا ، قم بالتنزيل من هنا VLC 2.2.6 Umbrella

Install بدلاً من القديم أو بجانبه (أي ، VLC الجديد) ، كما يحلو لك.

نبدأ ونرى شاشة فارغة مرة أخرى.

وهذا كله لأننا من الواضح أنه لم يتم تكوين استخدام برنامج الترميز H264. وبالتالي ، سيكون بإمكان VLC تحديد برنامج الترميز تلقائيًا إذا كان عليه التعامل مع الملف (في الإعدادات في البداية ، تم تحديد التحديد التلقائي). لكنهم يرمون دفق بايت عبر قناة واحدة ، وهناك العشرات من برامج الترميز التي يدعمها VLC. كيف يمكن معرفة أي واحد لتطبيق؟

لذلك ، نقوم بتثبيت برنامج الترميز بالقوة.



والآن نحن نستمتع ببث الفيديو "المباشر". الشيء الوحيد هو أنه لسبب ما يكمن في جانبه ، ولكن هذا تم إصلاحه بالفعل بسهولة في إعدادات مشغل الفيديو.

ويمكنك فقط بدء تشغيل اللاعب من سطر الأوامر باستخدام هذا المفتاح:

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

وسوف فك نفسها وتحول.


حتى يعمل يتدفقون. يبقى فقط لدمجه في نافذة JAVA لتطبيق التحكم الآلي. سنتعامل مع هذا قريبا جدا في الجزء الأخير.

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


All Articles