web-dev-qa-db-fra.com

Encodage H.264 à partir de la caméra avec Android MediaCodec

J'essaie de faire fonctionner cela sur Android 4.1 (en utilisant une tablette Asus Transformer mise à niveau). Merci à la réponse d'Alex à ma question précédente , j'ai déjà pu pour écrire des données H.264 brutes dans un fichier, mais ce fichier n'est jouable qu'avec ffplay -f h264, et il semble qu'il ait perdu toutes les informations concernant le framerate (lecture extrêmement rapide). De plus, l'espace colorimétrique semble incorrect (atm en utilisant la caméra par défaut du côté de l'encodeur).

public class AvcEncoder {

private MediaCodec mediaCodec;
private BufferedOutputStream outputStream;

public AvcEncoder() { 
    File f = new File(Environment.getExternalStorageDirectory(), "Download/video_encoded.264");
    touch (f);
    try {
        outputStream = new BufferedOutputStream(new FileOutputStream(f));
        Log.i("AvcEncoder", "outputStream initialized");
    } catch (Exception e){ 
        e.printStackTrace();
    }

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 320, 240);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
}

public void close() {
    try {
        mediaCodec.stop();
        mediaCodec.release();
        outputStream.flush();
        outputStream.close();
    } catch (Exception e){ 
        e.printStackTrace();
    }
}

// called from Camera.setPreviewCallbackWithBuffer(...) in other class
public void offerEncoder(byte[] input) {
    try {
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input);
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
            byte[] outData = new byte[bufferInfo.size];
            outputBuffer.get(outData);
            outputStream.write(outData, 0, outData.length);
            Log.i("AvcEncoder", outData.length + " bytes written");

            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        }
    } catch (Throwable t) {
        t.printStackTrace();
    }

}

Changer le type d'encodeur en "video/mp4" résout apparemment le problème de fréquence d'images, mais comme l'objectif principal est de créer un service de streaming, ce n'est pas une bonne solution.

Je sais que j'ai abandonné une partie du code d'Alex compte tenu du SPS et des PPS NALU, mais j'espérais que ce ne serait pas nécessaire car ces informations provenaient également de outData et j'ai supposé que l'encodeur formaterait cela correctement. Si ce n'est pas le cas, comment dois-je organiser les différents types de NALU dans mon fichier/flux?

Alors, qu'est-ce que je manque ici pour créer un flux H.264 valide et fonctionnel? Et quels paramètres dois-je utiliser pour faire une correspondance entre l'espace colorimétrique de l'appareil photo et l'espace colorimétrique de l'encodeur?

J'ai le sentiment qu'il s'agit plus d'une question liée au H.264 que d'un sujet Android/MediaCodec. Ou ne suis-je toujours pas en train d'utiliser correctement l'API MediaCodec?

Merci d'avance.

30
gleerman

Pour votre lecture rapide - problème de fréquence d'images, vous n'avez rien à faire ici. Comme il s'agit d'une solution de streaming, l'autre côté doit être informé à l'avance de la fréquence d'images ou des horodatages avec chaque image. Ces deux éléments ne font pas partie du flux élémentaire. Soit un framerate prédéterminé est choisi, soit vous transmettez un sdp ou quelque chose comme ça, soit vous utilisez des protocoles existants comme rtsp. Dans le second cas, les horodatages font partie du flux envoyé sous la forme de quelque chose comme rtp. Ensuite, le client doit dépayer le flux rtp et le lire bacl. Voici comment fonctionne le streaming élémentaire. [Fixez votre fréquence d'images si vous avez un encodeur à taux fixe ou donnez des horodatages]

La lecture PC locale sera rapide car elle ne connaîtra pas les fps. En donnant le paramètre fps avant l'entrée, par exemple

ffplay -fps 30 in.264

vous pouvez contrôler la lecture sur le PC.

Quant au fichier non jouable: a-t-il un SPS et un PPS. Vous devez également activer les en-têtes NAL - format annexe b. Je ne sais pas grand-chose sur Android, mais c'est une condition pour que tout flux élémentaire h.264 soit jouable lorsqu'il ne se trouve dans aucun conteneur et doit être vidé et joué plus tard. Si Android par défaut est mp4, mais les en-têtes annexb par défaut seront désactivés, il y a peut-être un interrupteur pour l'activer. Ou si vous obtenez des données image par image, ajoutez-les vous-même.

Quant au format couleur: je suppose que la valeur par défaut devrait fonctionner. Essayez donc de ne pas le régler. Sinon, essayez les formats 422 Planar ou UVYV/VYUY entrelacés. généralement, les caméras en font partie. (mais pas nécessaire, ce sont peut-être celles que j'ai rencontrées le plus souvent).

7
av501

Android 4.3 (API 18) fournit une solution simple. La classe MediaCodec accepte désormais les entrées de Surfaces, ce qui signifie que vous pouvez connecter l'aperçu de la surface de la caméra à l'encodeur et contourner tous les problèmes de format YUV étranges.

Il y a aussi un nouveau classe MediaMuxer qui convertira votre flux H.264 brut en un fichier .mp4 (éventuellement mélangé dans un flux audio).

Voir source CameraToMpegTest pour un exemple de faire exactement cela. (Il montre également l'utilisation d'un shader de fragments OpenGL ES pour effectuer un montage trivial sur la vidéo lors de son enregistrement.)

7
fadden

Vous pouvez convertir des espaces colorimétriques comme celui-ci, si vous avez défini l'espace colorimétrique d'aperçu sur YV12:

public static byte[] YV12toYUV420PackedSemiPlanar(final byte[] input, final byte[] output, final int width, final int height) {
        /* 
         * COLOR_TI_FormatYUV420PackedSemiPlanar is NV12
         * We convert by putting the corresponding U and V bytes together (interleaved).
         */
        final int frameSize = width * height;
        final int qFrameSize = frameSize/4;

        System.arraycopy(input, 0, output, 0, frameSize); // Y

        for (int i = 0; i < qFrameSize; i++) {
            output[frameSize + i*2] = input[frameSize + i + qFrameSize]; // Cb (U)
            output[frameSize + i*2 + 1] = input[frameSize + i]; // Cr (V)
        }
        return output;
    }

Ou

 public static byte[] YV12toYUV420Planar(byte[] input, byte[] output, int width, int height) {
        /* 
         * COLOR_FormatYUV420Planar is I420 which is like YV12, but with U and V reversed.
         * So we just have to reverse U and V.
         */
        final int frameSize = width * height;
        final int qFrameSize = frameSize/4;

        System.arraycopy(input, 0, output, 0, frameSize); // Y
        System.arraycopy(input, frameSize, output, frameSize + qFrameSize, qFrameSize); // Cr (V)
        System.arraycopy(input, frameSize + qFrameSize, output, frameSize, qFrameSize); // Cb (U)

        return output;
    }
6
br1

Vous pouvez interroger le MediaCodec pour son format bitmap pris en charge et interroger votre aperçu. Le problème est que certains MediaCodecs ne prennent en charge que les formats YUV intégrés exclusifs que vous ne pouvez pas obtenir à partir de l'aperçu. Particulièrement 2130706688 = 0x7F000100 = COLOR_TI_FormatYUV420PackedSemiPlanar. Le format par défaut de l'aperçu est 17 = NV21 = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar = YCbCr 420 Semi Planar

2
Marcus Wolschon

Si vous n'avez pas explicitement demandé un autre format de pixels, les tampons de prévisualisation de la caméra arriveront dans un format YUV 420 connu sous le nom NV21 , pour lequel COLOR_FormatYCrYCb est l'équivalent de MediaCodec.

Malheureusement, comme d'autres réponses sur cette page le mentionnent, il n'y a aucune garantie que sur votre appareil, l'encodeur AVC prend en charge ce format. Notez qu'il existe des périphériques étranges qui ne prennent pas en charge NV21, mais je n'en connais aucun qui puisse être mis à niveau vers API 16 (donc, avec MediaCodec).

La documentation de Google affirme également que YV12 YUV planaire doit être pris en charge comme format de prévisualisation de la caméra pour tous les appareils avec API> = 12. Par conséquent, il peut être utile de l'essayer (l'équivalent de MediaCodec est COLOR_FormatYUV420Planar que vous utilisez dans votre extrait de code).

Mise à jour : comme Andrew Cottrell me l'a rappelé, YV12 a encore besoin d'un échange de chrominance pour devenir COLOR_FormatYUV420Planar.

2
Alex Cohn