web-dev-qa-db-fra.com

Enregistrement de vidéos à l'aide de MediaCodec avec l'API Camera2

J'essaie d'utiliser MediaCodec pour enregistrer des images brutes d'ImageReader dans un rappel onImageAvailable, mais je ne parviens pas à écrire un code fonctionnel. La plupart des exemples utilisent l’API de l’appareil photo 1 ou MediaRecorder. Mon but est de capturer des images individuelles, de les traiter et de créer un MP4

Cadres YUV bruts

        @Override
        public void onImageAvailable(ImageReader reader) {
            Image i = reader.acquireLatestImage();
            processImage(i);
            i.close();
            Log.d("hehe", "onImageAvailable");
        }
    };

MediaCodec 

MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

Je ne parviens pas à relier le code indiqué sur https://developer.Android.com/reference/Android/media/MediaCodec . S'il vous plaît aider

8
HimalayanCoder

Vous devez créer une File d'attente , Poussez le tampon de votre image créé à partir des plans Image dans la file d'attente et traitez-le dans le dossier void onInputBufferAvailable(MediaCodec mc, int inputBufferId).

1) Créez une classe pour encapsuler les données du tampon:

class MyData{
    byte[] buffer;
    long presentationTimeUs;
    // to tell your encoder that is a EOS, otherwise you can not know when to stop
    boolean isEOS; 
    public MyData(byte[] buffer,long presentationTimeUs, boolean isEOS){
        this.buffer = new byte[buffer.length];
        System.arraycopy(buffer, 0, this.buffer, 0, buffer.length);
        this.presentationTimeUs = presentationTimeUs;
        this.isEOS = isEOS;
    }

    public byte[] getBuffer() {
        return buffer;
    }

    public void setBuffer(byte[] buffer) {
        this.buffer = buffer;
    }

    public long getPresentationTimeUs() {
        return presentationTimeUs;
    }

    public void setPresentationTimeUs(long presentationTimeUs) {
        this.presentationTimeUs = presentationTimeUs;
    }

    public boolean isEOS() {
        return isEOS;
    }

    public void setEOS(boolean EOS) {
        isEOS = EOS;
    }

}

2) Créer la file d'attente:

Queue<MyData> mQueue = new LinkedList<MyData>();

3) Convertir les plans d'image en tableau d'octets (octet []) à l'aide du code natif:

  • Ajout du support natif au fichier Gradle:

    Android {
    compileSdkVersion 27
    defaultConfig {
        ...
    
        externalNativeBuild {
            cmake {
                arguments "-DANDROID_STL=stlport_static"
                cppFlags "-std=c++11"
            }
        }
    }
    
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    ...
    

    • Création d'une fonction pour convertir les plans d'image en tableau d'octets: (native-yuv-to-buffer.cpp)}

    extern "C" JNIEXPORT jbyteArray JNICALL

Java_labs_farzi_camera2previewstream_MainActivity_yuvToBuffer (

JNIEnv *env,
jobject instance,
jobject yPlane,
jobject uPlane,
jobject vPlane,
jint yPixelStride,
jint yRowStride,
jint uPixelStride,
jint uRowStride,
jint vPixelStride,
jint vRowStride,
jint imgWidth,
jint imgHeight) {

    bbuf_yIn = static_cast<uint8_t *>(env->GetDirectBufferAddress(yPlane));
    bbuf_uIn = static_cast<uint8_t *>(env->GetDirectBufferAddress(uPlane));
    bbuf_vIn = static_cast<uint8_t *>(env->GetDirectBufferAddress(vPlane));

    buf = (uint8_t *) malloc(sizeof(uint8_t) * imgWidth * imgHeight +
                             2 * (imgWidth + 1) / 2 * (imgHeight + 1) / 2);

    bool isNV21;
    if (yPixelStride == 1) {
        // All pixels in a row are contiguous; copy one line at a time.
        for (int y = 0; y < imgHeight; y++)
            memcpy(buf + y * imgWidth, bbuf_yIn + y * yRowStride,
                   static_cast<size_t>(imgWidth));
    } else {
        // Highly improbable, but not disallowed by the API. In this case
        // individual pixels aren't stored consecutively but sparsely with
        // other data inbetween each pixel.
        for (int y = 0; y < imgHeight; y++)
            for (int x = 0; x < imgWidth; x++)
                buf[y * imgWidth + x] = bbuf_yIn[y * yRowStride + x * yPixelStride];
    }

    uint8_t *chromaBuf = &buf[imgWidth * imgHeight];
    int chromaBufStride = 2 * ((imgWidth + 1) / 2);
    if (uPixelStride == 2 && vPixelStride == 2 &&
        uRowStride == vRowStride && bbuf_vIn == bbuf_uIn + 1) {
        isNV21 = true;
        // The actual cb/cr planes happened to be laid out in
        // exact NV21 form in memory; copy them as is
        for (int y = 0; y < (imgHeight + 1) / 2; y++)
            memcpy(chromaBuf + y * chromaBufStride, bbuf_vIn + y * vRowStride,
                   static_cast<size_t>(chromaBufStride));
    } else if (vPixelStride == 2 && uPixelStride == 2 &&
               uRowStride == vRowStride && bbuf_vIn == bbuf_uIn + 1) {
        isNV21 = false;
        // The cb/cr planes happened to be laid out in exact NV12 form
        // in memory; if the destination API can use NV12 in addition to
        // NV21 do something similar as above, but using cbPtr instead of crPtr.
        // If not, remove this clause and use the generic code below.
    } else {
        isNV21 = true;
        if (vPixelStride == 1 && uPixelStride == 1) {
            // Continuous cb/cr planes; the input data was I420/YV12 or similar;
            // copy it into NV21 form
            for (int y = 0; y < (imgHeight + 1) / 2; y++) {
                for (int x = 0; x < (imgWidth + 1) / 2; x++) {
                    chromaBuf[y * chromaBufStride + 2 * x + 0] = bbuf_vIn[y * vRowStride + x];
                    chromaBuf[y * chromaBufStride + 2 * x + 1] = bbuf_uIn[y * uRowStride + x];
                }
            }
        } else {
            // Generic data copying into NV21
            for (int y = 0; y < (imgHeight + 1) / 2; y++) {
                for (int x = 0; x < (imgWidth + 1) / 2; x++) {
                    chromaBuf[y * chromaBufStride + 2 * x + 0] = bbuf_vIn[y * vRowStride +
                                                                          x * uPixelStride];
                    chromaBuf[y * chromaBufStride + 2 * x + 1] = bbuf_uIn[y * uRowStride +
                                                                          x * vPixelStride];
                }
            }
        }
    }

    uint8_t *I420Buff = (uint8_t *) malloc(sizeof(uint8_t) * imgWidth * imgHeight +
                                           2 * (imgWidth + 1) / 2 * (imgHeight + 1) / 2);
    SPtoI420(buf,I420Buff,imgWidth,imgHeight,isNV21);

    jbyteArray ret = env->NewByteArray(imgWidth * imgHeight *
                                       3/2);
    env->SetByteArrayRegion (ret, 0, imgWidth * imgHeight *
                                     3/2, (jbyte*)I420Buff);
    free(buf);
    free (I420Buff);
    return ret;
}
  • Ajout d'une fonction pour convertir semi-planaire en planaire: 

    booléen SPtoI420 (const uint8_t * src, uint8_t * dst, int largeur, int hauteur, bool isNV21) { si (! src ||! dst) { retourne faux; }

    unsigned int YSize = width * height;
    unsigned int UVSize = (YSize>>1);
    
    // NV21: Y..Y + VUV...U
    const uint8_t *pSrcY = src;
    const uint8_t *pSrcUV = src + YSize;
    
    // I420: Y..Y + U.U + V.V
    uint8_t *pDstY = dst;
    uint8_t *pDstU = dst + YSize;
    uint8_t *pDstV = dst + YSize + (UVSize>>1);
    
    // copy Y
    memcpy(pDstY, pSrcY, YSize);
    
    // copy U and V
    for (int k=0; k < (UVSize>>1); k++) {
        if(isNV21) {
            pDstV[k] = pSrcUV[k * 2];     // copy V
            pDstU[k] = pSrcUV[k * 2 + 1];   // copy U
        }else{
            pDstU[k] = pSrcUV[k * 2];     // copy V
            pDstV[k] = pSrcUV[k * 2 + 1];   // copy U
        }
    }
    
    return true;}
    

4) Poussez votre tampon dans la file d'attente:

private final ImageReader.OnImageAvailableListener mOnGetPreviewListener
    = new ImageReader.OnImageAvailableListener() {

@Override
public void onImageAvailable(ImageReader reader) {
    Image image = reader.acquireLatestImage();
    if (image == null)
        return;
    final Image.Plane[] planes = image.getPlanes();
    Image.Plane yPlane = planes[0];
    Image.Plane uPlane = planes[1];
    Image.Plane vPlane = planes[2];
    byte[] mBuffer = yuvToBuffer(yPlane.getBuffer(),
            uPlane.getBuffer(),
            vPlane.getBuffer(),
            yPlane.getPixelStride(),
            yPlane.getRowStride(),
            uPlane.getPixelStride(),
            uPlane.getRowStride(),
            vPlane.getPixelStride(),
            vPlane.getRowStride(),
            image.getWidth(),
            image.getHeight());
    mQueue.add(new MyData(mBuffer, image.getTimestamp(), false));
    image.close();
    Log.d("hehe", "onImageAvailable");
}

};


5) Encode les données et enregistre un fichier vidéo h264 (VLC pour le lire):

        public void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
        ByteBuffer inputBuffer = mc.getInputBuffer(inputBufferId);
        Log.d(TAG, "onInputBufferAvailable: ");
        // fill inputBuffer with valid data
        MyData data = mQueue.poll();
        if (data != null) {
            // check if is EOS and process with EOS flag if is the case
            // else if NOT EOS
            if (inputBuffer != null) {
                Log.e(TAG, "onInputBufferAvailable: "+data.getBuffer().length);
                inputBuffer.clear();
                inputBuffer.put(data.getBuffer());

                mc.queueInputBuffer(inputBufferId,
                        0,
                        data.getBuffer().length,
                        data.getPresentationTimeUs(),
                        0);
            }

        } else {

            mc.queueInputBuffer(inputBufferId,
                    0,
                    0,
                    0,
                    0);
        }
    }

    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        Log.d(TAG, "onOutputBufferAvailable: ");
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        byte[] outData = new byte[info.size];
        if (outputBuffer != null) {
            outputBuffer.get(outData);
            try {
                fos.write(outData);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        codec.releaseOutputBuffer(index,false);
    }

6) Mux votre piste dans la void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …), le traitement est similaire aux exemples avec le mode synchrone que vous pouvez trouver sur Internet.

J'espère que ma réponse vous aidera

Exemple de code complet ici

1
E.Abdel

Pourquoi ne pas essayer cet exemple: https://github.com/googlesamples/Android-Camera2Video

Je pense que cela répondra certainement à toutes vos exigences et vous pouvez toujours me contacter si vous êtes incapable de vous identifier au code de l'exemple mentionné ci-dessus.

Cet exemple utilise l'API Camera2, ainsi que la conversion de trames YUV brutes que vous souhaitez, ce que vous pouvez utiliser. J'espère donc que vous ne rencontrerez aucun problème si vous parcouriez l'échantillon une fois et utilisiez son code pour enregistrer des vidéos MP4 dans l'application de votre choix.

Par exemple -a) Pour cela, vous devrez implémenter un CameraDevice.StateCallback pour recevoir des événements sur les modifications de l'état du périphérique de la caméra. Redéfinissez ses méthodes pour définir votre instance CameraDevice, démarrez l'aperçu, puis arrêtez et relâchez l'appareil photo.

b) Lorsque l'aperçu est lancé, configurez MediaRecorder pour accepter le format vidéo.

c) Ensuite, configurez un CaptureRequest.Builder en utilisant createCaptureRequest (CameraDevice.TEMPLATE_RECORD) sur votre instance CameraDevice.

d) Ensuite, implémentez un CameraCaptureSession.StateCallback en utilisant la méthode createCaptureSession (surfaces, nouvelle CameraCaptureSession.StateCallback () {}) sur votre instance CameraDevice, où surfaces est une liste constituée TextureView et la surface de votre instance MediaRecorder.

e) Utilisez les méthodes start () et stop () sur votre instance MediaRecorder pour démarrer et arrêter réellement l'enregistrement.

f) Enfin, configurez et nettoyez votre appareil photo dans onResume () et onPause ().

Bonne codage.

1
Nitin Gurbani