Muxing audio AAC avec MediaCodec et MediaMuxer D'Android
je modifie un Android Cadre exemple pour empaqueter les flux AAC élémentaires produits par MediaCodec dans un standalone .fichier mp4. Je suis à l'aide d'un seul MediaMuxer
instance contenant une piste AAC générée par un MediaCodec
instance.
cependant je reçois toujours un message d'erreur sur un appel à mMediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)
:
E/MPEG4Writer﹕timestampUs 0 < lastTimestampUs XXXXX for Audio track
quand je mets en file d'attente les données brutes dans mCodec.queueInputBuffer(...)
je fournis 0 comme valeur timestamp par le Framework Exemple (j'ai aussi essayé d'utiliser des valeurs de timestamp monotones avec le même résultat. J'ai encodé avec succès des cadres de caméra raw vers des fichiers h264/mp4 avec cette même méthode).
consulter la totalité du code source
extrait le plus pertinent:
private static void testEncoder(String componentName, MediaFormat format, Context c) {
int trackIndex = 0;
boolean mMuxerStarted = false;
File f = FileUtils.createTempFileInRootAppStorage(c, "aac_test_" + new Date().getTime() + ".mp4");
MediaCodec codec = MediaCodec.createByCodecName(componentName);
try {
codec.configure(
format,
null /* surface */,
null /* crypto */,
MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IllegalStateException e) {
Log.e(TAG, "codec '" + componentName + "' failed configuration.");
}
codec.start();
try {
mMediaMuxer = new MediaMuxer(f.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException ioe) {
throw new RuntimeException("MediaMuxer creation failed", ioe);
}
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
int numBytesSubmitted = 0;
boolean doneSubmittingInput = false;
int numBytesDequeued = 0;
while (true) {
int index;
if (!doneSubmittingInput) {
index = codec.dequeueInputBuffer(kTimeoutUs /* timeoutUs */);
if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
if (numBytesSubmitted >= kNumInputBytes) {
Log.i(TAG, "queueing EOS to inputBuffer");
codec.queueInputBuffer(
index,
0 /* offset */,
0 /* size */,
0 /* timeUs */,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
if (VERBOSE) {
Log.d(TAG, "queued input EOS.");
}
doneSubmittingInput = true;
} else {
int size = queueInputBuffer(
codec, codecInputBuffers, index);
numBytesSubmitted += size;
if (VERBOSE) {
Log.d(TAG, "queued " + size + " bytes of input data.");
}
}
}
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
index = codec.dequeueOutputBuffer(info, kTimeoutUs /* timeoutUs */);
if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = codec.getOutputFormat();
trackIndex = mMediaMuxer.addTrack(newFormat);
mMediaMuxer.start();
mMuxerStarted = true;
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
} else {
// Write to muxer
ByteBuffer encodedData = codecOutputBuffers[index];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + index +
" was null");
}
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
info.size = 0;
}
if (info.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
mMediaMuxer.writeSampleData(trackIndex, encodedData, info);
if (VERBOSE) Log.d(TAG, "sent " + info.size + " audio bytes to muxer with pts " + info.presentationTimeUs);
}
codec.releaseOutputBuffer(index, false);
// End write to muxer
numBytesDequeued += info.size;
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE) {
Log.d(TAG, "dequeued output EOS.");
}
break;
}
if (VERBOSE) {
Log.d(TAG, "dequeued " + info.size + " bytes of output data.");
}
}
}
if (VERBOSE) {
Log.d(TAG, "queued a total of " + numBytesSubmitted + "bytes, "
+ "dequeued " + numBytesDequeued + " bytes.");
}
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int inBitrate = sampleRate * channelCount * 16; // bit/sec
int outBitrate = format.getInteger(MediaFormat.KEY_BIT_RATE);
float desiredRatio = (float)outBitrate / (float)inBitrate;
float actualRatio = (float)numBytesDequeued / (float)numBytesSubmitted;
if (actualRatio < 0.9 * desiredRatio || actualRatio > 1.1 * desiredRatio) {
Log.w(TAG, "desiredRatio = " + desiredRatio
+ ", actualRatio = " + actualRatio);
}
codec.release();
mMediaMuxer.stop();
mMediaMuxer.release();
codec = null;
}
mise à Jour: j'ai trouvé un symptôme de racine que je pense se trouve dans MediaCodec
.:
j'ai envoyer presentationTimeUs=1000
queueInputBuffer(...)
mais recevoir info.presentationTimeUs= 33219
après l'appel de MediaCodec.dequeueOutputBuffer(info, timeoutUs)
. fadden j'ai laissé un commentaire utile concernant ce comportement.
3 réponses
merci à l'aide de fadden j'ai une preuve de concept encoder audio et video+audio codeur sur Github. En résumé:
Envoyer AudioRecord
's des échantillons à un MediaCodec
+ MediaMuxer
wrapper. Utiliser l'heure du système à audioRecord.read(...)
fonctionne suffisamment bien qu'un timestamp audio, pourvu que vous polliez assez souvent pour éviter de remplir le tampon interne D'AudioRecord (pour éviter de dériver entre le moment où vous appelez lire et le moment où AudioRecord a enregistré les échantillons). Trop un mauvais AudioRecord ne communique pas directement les horodateurs...
// Setup AudioRecord
while (isRecording) {
audioPresentationTimeNs = System.nanoTime();
audioRecord.read(dataBuffer, 0, samplesPerFrame);
hwEncoder.offerAudioEncoder(dataBuffer.clone(), audioPresentationTimeNs);
}
notez que AudioRecord garantit seulement le support des échantillons PCM 16 bits, bien que MediaCodec.queueInputBuffer
entrée byte[]
. Le passage d'un byte[]
audioRecord.read(dataBuffer,...)
tronquer diviser les échantillons de 16 bits en 8 bits pour vous.
j'ai trouvé que le sondage de cette façon encore occasionnellement généré un timestampUs XXX < lastTimestampUs XXX for Audio track
erreur, donc j'ai inclus un peu de logique pour garder la trace du bufferInfo.presentationTimeUs
rapporté par mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutMs)
et d'ajuster, si nécessaire, avant d'appeler mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)
.
Le code de la réponse ci-dessus https://stackoverflow.com/a/18966374/6463821 fournit également timestampUs XXX < lastTimestampUs XXX for Audio track
erreur, parce que si vous lisez à partir D'AudioRecord tampon plus rapide alors besoin, la durée entre les timstamps générés sera plus petite que la durée réelle entre les échantillons audio.
donc ma solution pour ce problème est de générer le premier timstamp et chaque prochain échantillon augmente le timestamp en fonction de la durée de votre échantillon ( dépend du débit, du format audio, du canal config).
BUFFER_DURATION_US = 1_000_000 * (ARR_SIZE / AUDIO_CHANNELS) / SAMPLE_AUDIO_RATE_IN_HZ;
...
long firstPresentationTimeUs = System.nanoTime() / 1000;
...
audioRecord.read(shortBuffer, OFFSET, ARR_SIZE);
long presentationTimeUs = count++ * BUFFER_DURATION + firstPresentationTimeUs;
la lecture à partir de L'AudioRecord doit être dans un thread séparé, et tous les tampons de lecture doivent être ajoutés à la file d'attente sans attendre l'encodage ou toute autre action avec eux, pour éviter la perte d'échantillons audio.
worker =
new Thread() {
@Override
public void run() {
try {
AudioFrameReader reader =
new AudioFrameReader(audioRecord);
while (!isInterrupted()) {
Thread.sleep(10);
addToQueue(
reader
.read());
}
} catch (InterruptedException e) {
Log.w(TAG, "run: ", e);
}
}
};
problème survenu parce que vous recevez des tampons désordonnés : Essayez d'ajouter le test suivant :
if(lastAudioPresentationTime == -1) {
lastAudioPresentationTime = bufferInfo.presentationTimeUs;
}
else if (lastAudioPresentationTime < bufferInfo.presentationTimeUs) {
lastAudioPresentationTime = bufferInfo.presentationTimeUs;
}
if ((bufferInfo.size != 0) && (lastAudioPresentationTime <= bufferInfo.presentationTimeUs)) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
mMuxer.writeSampleData(trackIndex.index, encodedData, bufferInfo);
}
encoder.releaseOutputBuffer(encoderStatus, false);