web-dev-qa-db-fra.com

Android MediaCodec Encoder et décoder en mode asynchrone

J'essaie de décoder une vidéo à partir d'un fichier et de l'encoder dans un format différent avec MediaCodec dans le nouveau mode asynchrone pris en charge dans les API de niveau 21 et supérieur (Android OS 5.0 Lollipop).

Il existe de nombreux exemples pour ce faire dans mode synchrone sur des sites tels que Big Flake , Google Grafika et dans des dizaines de réponses sur StackOverflow, mais aucune d'entre elles ne prend en charge le mode asynchrone.

Je n'ai pas besoin d'afficher la vidéo pendant le processus.

Je crois que la procédure générale consiste à lire le fichier avec un MediaExtractor comme entrée dans un MediaCodecdécodeur), à permettre à la sortie du décodeur d'être rendue dans un Surface qui est également l'entrée partagée dans un MediaCodec (encodeur), puis enfin écrire le fichier de sortie de l’Encodeur via une MediaMuxer. La Surface est créée lors de l’installation du codeur et partagée avec le décodeur.

Je peux décoder la vidéo en une TextureView, mais le partage de la Surface avec le codeur au lieu de l'écran n'a pas abouti.

J'ai configuré MediaCodec.Callback()s pour mes deux codecs. Je pense que l'un des problèmes est que je ne sais pas quoi faire dans la fonction onInputBufferAvailable() du rappel du codeur. Je ne sais pas quoi (ou savoir comment) copier les données de la Surface dans le codeur - cela devrait se faire automatiquement (comme c'est le cas sur la sortie du décodeur avec codec.releaseOutputBuffer(outputBufferId, true);). Pourtant, je crois que onInputBufferAvailable nécessite un appel à codec.queueInputBuffer pour fonctionner. Je ne sais tout simplement pas comment définir les paramètres sans obtenir des données provenant de quelque chose comme une MediaExtractor telle qu'utilisée du côté décodage.

Si vous avez un _ (Exemple qui ouvre un fichier vidéo, le décode, le convertit dans une résolution ou un format différent à l'aide des rappels asynchrones MediaCodec, puis l'enregistre sous la forme d'un fichier, partagez votre exemple de code.

=== EDIT===

Voici un exemple de travail en mode synchrone de ce que j'essaie de faire en mode asynchrone: ExtractDecodeEditEncodeMuxTest.Java: https://Android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests /media/src/Android/media/cts/ExtractDecodeEditEncodeMuxTest.Java Cet exemple fonctionne dans mon application.

Android MediaCodec

11
David Manpearl

Je crois que vous ne devriez rien avoir à faire dans le rappel onInputBufferAvailable() de l'encodeur - vous ne devriez pas appeler encoder.queueInputBuffer(). De même que vous n’appelez jamais encoder.dequeueInputBuffer() et encoder.queueInputBuffer() manuellement lorsque vous effectuez un codage d’entrée Surface en mode synchrone, vous ne devez pas le faire en mode asynchrone.

Lorsque vous appelez decoder.releaseOutputBuffer(outputBufferId, true); (en mode synchrone et asynchrone), cette dernière (en utilisant la variable Surface que vous avez fournie) met en file d'attente un tampon d'entrée de la surface, restitue la sortie et la remet en file d'attente sur la surface (dans le codeur). La seule différence entre le mode synchrone et le mode asynchrone réside dans la façon dont les événements de mémoire tampon sont exposés dans l'API publique. Toutefois, lorsque vous utilisez l'entrée Surface, une API différente (interne) est utilisée pour accéder au même système. Par conséquent, le mode synchrone ou asynchrone n'a pas d'importance. cela du tout.

Donc, autant que je sache (même si je n'ai pas essayé moi-même), vous devriez simplement laisser le callback onInputBufferAvailable() vide pour le codeur.

EDIT: Alors, j’ai essayé de le faire moi-même, et c’est (presque) aussi simple que décrit ci-dessus.

Si la surface d'entrée du codeur est configurée directement en sortie du décodeur (sans SurfaceTexture entre les deux), tout fonctionne, avec une boucle de décodage-codage synchrone convertie en une boucle asynchrone.

Si vous utilisez SurfaceTexture, cependant, vous risquez de vous retrouver pris au piège. Il y a un problème avec la façon dont on attend que les trames arrivent à SurfaceTexture par rapport au thread appelant, voir https://Android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests /media/src/Android/media/cts/DecodeEditEncodeTest.Java#106 et https://Android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/tests/media/src /Android/media/cts/EncodeDecodeTest.Java#104 et https://Android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/Android/media /cts/OutputSurface.Java#113 pour les références à cela.

Pour moi, le problème se trouve dans awaitNewImage comme dans https://Android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/Android/media /cts/OutputSurface.Java#240 . Si le callback onFrameAvailable est supposé être appelé sur le thread principal, nous avons un problème si l'appel awaitNewImage est également exécuté sur le thread principal. Si les callbacks onOutputBufferAvailable sont également appelés sur le thread principal et que vous appelez awaitNewImage à partir de là, nous avons un problème, car vous finirez par attendre un rappel (avec une wait() qui bloque tout le thread) qui ne peut être exécuté avant la méthode en cours revient.

Nous devons donc nous assurer que les rappels onFrameAvailable arrivent sur un autre thread que celui qui appelle awaitNewImage. Une façon assez simple de faire cela est de créer un nouveau thread séparé, qui ne fait que servir les rappels onFrameAvailable. Pour ce faire, vous pouvez par exemple faire ce:

    private HandlerThread mHandlerThread = new HandlerThread("CallbackThread");
    private Handler mHandler;
...
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
...
        mSurfaceTexture.setOnFrameAvailableListener(this, mHandler);

J'espère que cela vous suffit pour que vous puissiez résoudre votre problème. Faites-moi savoir si vous souhaitez que je modifie l'un des exemples publics pour y implémenter des rappels asynchrones.

EDIT2: De plus, étant donné que le rendu GL peut être effectué à partir du rappel onOutputBufferAvailable, il peut s'agir d'un thread différent de celui qui a configuré le contexte EGL. Donc, dans ce cas, il faut libérer le contexte EGL dans le thread qui l'a configuré, comme ceci:

mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);

Et rattachez-le dans l'autre thread avant de rendre:

mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);

EDIT3: De plus, si les rappels du codeur et du décodeur sont reçus sur le même fil, le décodeur onOutputBufferAvailable qui rend le rendu peut empêcher les rappels du codeur d'être remis. S'ils ne sont pas livrés, le rendu peut être bloqué indéfiniment car l'encodeur ne renvoie pas les tampons de sortie. Cela peut être corrigé en s'assurant que les rappels du décodeur vidéo sont reçus sur un autre thread, ce qui évite le problème avec le rappel onFrameAvailable.

J'ai essayé d'implémenter tout cela au-dessus de ExtractDecodeEditEncodeMuxTest, et je me suis bien débrouillé, jetez un coup d'œil à https://github.com/mstorsjo/Android-decodeencodetest . Au départ, j'avais importé le test inchangé, puis converti séparément le mode asynchrone et corrigé les détails complexes afin de faciliter l'examen des correctifs individuels dans le journal de validation.

12
mstorsjo

Peut également définir le gestionnaire dans le MediaEncoder.

---> AudioEncoderCallback (aacSamplePreFrameSize), mHandler);


MyAudioCodecWrapper myMediaCodecWrapper;

public MyAudioEncoder(long startRecordWhenNs){
    super.startRecordWhenNs = startRecordWhenNs;
}

@RequiresApi(api = Build.VERSION_CODES.M)
public MyAudioCodecWrapper prepareAudioEncoder(AudioRecord _audioRecord , int aacSamplePreFrameSize)  throws Exception{
    if(_audioRecord==null || aacSamplePreFrameSize<=0)
        throw new Exception();

    audioRecord = _audioRecord;
    Log.d(TAG, "audioRecord:" + audioRecord.getAudioFormat() + ",aacSamplePreFrameSize:" + aacSamplePreFrameSize);

    mHandlerThread.start();
    mHandler = new Handler(mHandlerThread.getLooper());

    MediaFormat audioFormat = new MediaFormat();
    audioFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_AUDIO_AAC);
    //audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE );
    audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
    audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, audioRecord.getSampleRate());//44100
    audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, audioRecord.getChannelCount());//1(單身道)
    audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
    audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384);
    MediaCodec codec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
    codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    codec.setCallback(new AudioEncoderCallback(aacSamplePreFrameSize),mHandler);
    //codec.start();

    MyAudioCodecWrapper myMediaCodecWrapper = new MyAudioCodecWrapper();
    myMediaCodecWrapper.mediaCodec = codec;

    super.mediaCodec = codec;

    return myMediaCodecWrapper;

}
1
user8270308