解码时的 MediaCodec AV 同步
MediaCodec AV Sync when decoding
所有关于同步音频和视频的问题,当使用 MediaCodec
解码时,建议我们应该使用 "AV Sync" 机制来使用时间戳同步视频和音频。
这是我为实现这一目标所做的工作:
我有 2 个线程,一个用于解码视频,一个用于解码音频。要同步视频和音频,我正在使用 Extractor.getSampleTime()
来确定我是否应该释放音频或视频缓冲区,请参见下文:
//This is called after configuring MediaCodec(both audio and video)
private void startPlaybackThreads(){
//Audio playback thread
mAudioWorkerThread = new Thread("AudioThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
//Check info below
if (shouldPushAudio()) {
workLoopAudio();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mAudioWorkerThread.start();
//Video playback thread
mVideoWorkerThread = new Thread("VideoThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
//Check info below
if (shouldPushVideo()) {
workLoopVideo();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mVideoWorkerThread.start();
}
//Check if more buffers should be sent to the audio decoder
private boolean shouldPushAudio(){
int audioTime =(int) mAudioExtractor.getSampleTime();
int videoTime = (int) mExtractor.getSampleTime();
return audioTime <= videoTime;
}
//Check if more buffers should be sent to the video decoder
private boolean shouldPushVideo(){
int audioTime =(int) mAudioExtractor.getSampleTime();
int videoTime = (int) mExtractor.getSampleTime();
return audioTime > videoTime;
}
workLoopAudio()
和 workLoopVideo()
里面是我所有的 MediaCodec
逻辑(我决定不 post 因为它不相关)。
所以我所做的是,获取视频和音轨的采样时间,然后检查哪个更大(更靠前)。如果视频是 "ahead",那么我会将更多缓冲区传递给我的音频解码器,反之亦然。
这似乎工作正常 - 视频和音频正在同步播放。
我的问题:
我想知道我的做法是否正确(我们应该这样做,还是有another/better方式)?我找不到这方面的任何工作示例(写在java/kotlin),因此问题。
编辑 1:
当我 decode/play 使用 FFmpeg
编码的视频时,我发现音频落后于视频(非常轻微)。如果我使用未使用 FFmpeg
编码的视频,则视频和音频会完美同步。
FFmpeg
命令没有异常:
-i inputPath -crf 18 -c:v libx264 -preset ultrafast OutputPath
我将在下面提供更多信息:
我initialize/createAudioTrack
是这样的:
//Audio
mAudioExtractor = new MediaExtractor();
mAudioExtractor.setDataSource(mSource);
int audioTrackIndex = selectAudioTrack(mAudioExtractor);
if (audioTrackIndex < 0){
throw new IOException("Can't find Audio info!");
}
mAudioExtractor.selectTrack(audioTrackIndex);
mAudioFormat = mAudioExtractor.getTrackFormat(audioTrackIndex);
mAudioMime = mAudioFormat.getString(MediaFormat.KEY_MIME);
mAudioChannels = mAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mAudioSampleRate = mAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
final int min_buf_size = AudioTrack.getMinBufferSize(mAudioSampleRate, (mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO), AudioFormat.ENCODING_PCM_16BIT);
final int max_input_size = mAudioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
mAudioInputBufSize = min_buf_size > 0 ? min_buf_size * 4 : max_input_size;
if (mAudioInputBufSize > max_input_size) mAudioInputBufSize = max_input_size;
final int frameSizeInBytes = mAudioChannels * 2;
mAudioInputBufSize = (mAudioInputBufSize / frameSizeInBytes) * frameSizeInBytes;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
mAudioSampleRate,
(mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
AudioFormat.ENCODING_PCM_16BIT,
AudioTrack.getMinBufferSize(mAudioSampleRate, mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT),
AudioTrack.MODE_STREAM);
try {
mAudioTrack.play();
} catch (final Exception e) {
Log.e(TAG, "failed to start audio track playing", e);
mAudioTrack.release();
mAudioTrack = null;
}
然后我这样给 AudioTrack
写信:
//Called from within workLoopAudio, when releasing audio buffers
if (bufferAudioIndex >= 0) {
if (mAudioBufferInfo.size > 0) {
internalWriteAudio(mAudioOutputBuffers[bufferAudioIndex], mAudioBufferInfo.size);
}
mAudioDecoder.releaseOutputBuffer(bufferAudioIndex, false);
}
private boolean internalWriteAudio(final ByteBuffer buffer, final int size) {
if (mAudioOutTempBuf.length < size) {
mAudioOutTempBuf = new byte[size];
}
buffer.position(0);
buffer.get(mAudioOutTempBuf, 0, size);
buffer.clear();
if (mAudioTrack != null)
mAudioTrack.write(mAudioOutTempBuf, 0, size);
return true;
}
"NEW" 问题:
如果我使用使用 FFmpeg
编码的视频,音频会落后于视频约 200 毫秒,是否有发生这种情况的原因?
现在好像可以用了。我使用与上面相同的逻辑,但现在我在调用 dequeueOutputBuffer
之前保留从 MediaCodec.BufferInfo()
返回的 presentationTimeUs
的引用以检查我是否应该继续我的视频或音频工作循环:
// Check if audio work loop should continue
private boolean shouldPushAudio(){
long videoTime = mExtractor.getSampleTime();
return tempAudioPresentationTimeUs <= videoTime;
}
// Check if video work loop should continue
private boolean shouldPushVideo(){
long videoTime = mExtractor.getSampleTime();
return tempAudioPresentationTimeUs >= videoTime;
}
// tempAudioPresentationTimeUs is set right before I call dequeueOutputBuffer
// As shown here:
tempAudioPresentationTimeUs = mAudioBufferInfo.presentationTimeUs;
int outIndex = mAudioDecoder.dequeueOutputBuffer(mAudioBufferInfo, timeout);
通过这样做,我的视频和音频完美同步,即使是使用 FFmpeg
编码的文件(如我在上面的编辑中提到的)。
我 运行 遇到了视频工作循环未完成的问题,这是由于音频在视频之前到达 EOS 然后返回 -1
造成的。所以我把原来的 mVideoWorkerThread
改成了下面的:
mVideoWorkerThread = new Thread("VideoThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
if (shouldPushVideo() || audioReachedEOS()) {
workLoopVideo();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mVideoWorkerThread.start();
private boolean audioReachedEOS() {
return mAudioExtractor.getSampleTime() == -1;
}
所以我使用 audioReachedEOS()
来检查我的音频是否 MediaExtractor
returns -1
。如果是,则表示我的音频已完成,但我的视频尚未完成,因此我会继续我的视频工作循环,直到完成。
这似乎按预期工作(当我只 play/pause 视频而没有搜索时)。 seeking还有个问题,我就不细说了。
我会按原样发布我的应用程序,并在我 运行 遇到问题时更新此答案。
所有关于同步音频和视频的问题,当使用 MediaCodec
解码时,建议我们应该使用 "AV Sync" 机制来使用时间戳同步视频和音频。
这是我为实现这一目标所做的工作:
我有 2 个线程,一个用于解码视频,一个用于解码音频。要同步视频和音频,我正在使用 Extractor.getSampleTime()
来确定我是否应该释放音频或视频缓冲区,请参见下文:
//This is called after configuring MediaCodec(both audio and video)
private void startPlaybackThreads(){
//Audio playback thread
mAudioWorkerThread = new Thread("AudioThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
//Check info below
if (shouldPushAudio()) {
workLoopAudio();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mAudioWorkerThread.start();
//Video playback thread
mVideoWorkerThread = new Thread("VideoThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
//Check info below
if (shouldPushVideo()) {
workLoopVideo();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mVideoWorkerThread.start();
}
//Check if more buffers should be sent to the audio decoder
private boolean shouldPushAudio(){
int audioTime =(int) mAudioExtractor.getSampleTime();
int videoTime = (int) mExtractor.getSampleTime();
return audioTime <= videoTime;
}
//Check if more buffers should be sent to the video decoder
private boolean shouldPushVideo(){
int audioTime =(int) mAudioExtractor.getSampleTime();
int videoTime = (int) mExtractor.getSampleTime();
return audioTime > videoTime;
}
workLoopAudio()
和 workLoopVideo()
里面是我所有的 MediaCodec
逻辑(我决定不 post 因为它不相关)。
所以我所做的是,获取视频和音轨的采样时间,然后检查哪个更大(更靠前)。如果视频是 "ahead",那么我会将更多缓冲区传递给我的音频解码器,反之亦然。
这似乎工作正常 - 视频和音频正在同步播放。
我的问题:
我想知道我的做法是否正确(我们应该这样做,还是有another/better方式)?我找不到这方面的任何工作示例(写在java/kotlin),因此问题。
编辑 1:
当我 decode/play 使用 FFmpeg
编码的视频时,我发现音频落后于视频(非常轻微)。如果我使用未使用 FFmpeg
编码的视频,则视频和音频会完美同步。
FFmpeg
命令没有异常:
-i inputPath -crf 18 -c:v libx264 -preset ultrafast OutputPath
我将在下面提供更多信息:
我initialize/createAudioTrack
是这样的:
//Audio
mAudioExtractor = new MediaExtractor();
mAudioExtractor.setDataSource(mSource);
int audioTrackIndex = selectAudioTrack(mAudioExtractor);
if (audioTrackIndex < 0){
throw new IOException("Can't find Audio info!");
}
mAudioExtractor.selectTrack(audioTrackIndex);
mAudioFormat = mAudioExtractor.getTrackFormat(audioTrackIndex);
mAudioMime = mAudioFormat.getString(MediaFormat.KEY_MIME);
mAudioChannels = mAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mAudioSampleRate = mAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
final int min_buf_size = AudioTrack.getMinBufferSize(mAudioSampleRate, (mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO), AudioFormat.ENCODING_PCM_16BIT);
final int max_input_size = mAudioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
mAudioInputBufSize = min_buf_size > 0 ? min_buf_size * 4 : max_input_size;
if (mAudioInputBufSize > max_input_size) mAudioInputBufSize = max_input_size;
final int frameSizeInBytes = mAudioChannels * 2;
mAudioInputBufSize = (mAudioInputBufSize / frameSizeInBytes) * frameSizeInBytes;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
mAudioSampleRate,
(mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
AudioFormat.ENCODING_PCM_16BIT,
AudioTrack.getMinBufferSize(mAudioSampleRate, mAudioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT),
AudioTrack.MODE_STREAM);
try {
mAudioTrack.play();
} catch (final Exception e) {
Log.e(TAG, "failed to start audio track playing", e);
mAudioTrack.release();
mAudioTrack = null;
}
然后我这样给 AudioTrack
写信:
//Called from within workLoopAudio, when releasing audio buffers
if (bufferAudioIndex >= 0) {
if (mAudioBufferInfo.size > 0) {
internalWriteAudio(mAudioOutputBuffers[bufferAudioIndex], mAudioBufferInfo.size);
}
mAudioDecoder.releaseOutputBuffer(bufferAudioIndex, false);
}
private boolean internalWriteAudio(final ByteBuffer buffer, final int size) {
if (mAudioOutTempBuf.length < size) {
mAudioOutTempBuf = new byte[size];
}
buffer.position(0);
buffer.get(mAudioOutTempBuf, 0, size);
buffer.clear();
if (mAudioTrack != null)
mAudioTrack.write(mAudioOutTempBuf, 0, size);
return true;
}
"NEW" 问题:
如果我使用使用 FFmpeg
编码的视频,音频会落后于视频约 200 毫秒,是否有发生这种情况的原因?
现在好像可以用了。我使用与上面相同的逻辑,但现在我在调用 dequeueOutputBuffer
之前保留从 MediaCodec.BufferInfo()
返回的 presentationTimeUs
的引用以检查我是否应该继续我的视频或音频工作循环:
// Check if audio work loop should continue
private boolean shouldPushAudio(){
long videoTime = mExtractor.getSampleTime();
return tempAudioPresentationTimeUs <= videoTime;
}
// Check if video work loop should continue
private boolean shouldPushVideo(){
long videoTime = mExtractor.getSampleTime();
return tempAudioPresentationTimeUs >= videoTime;
}
// tempAudioPresentationTimeUs is set right before I call dequeueOutputBuffer
// As shown here:
tempAudioPresentationTimeUs = mAudioBufferInfo.presentationTimeUs;
int outIndex = mAudioDecoder.dequeueOutputBuffer(mAudioBufferInfo, timeout);
通过这样做,我的视频和音频完美同步,即使是使用 FFmpeg
编码的文件(如我在上面的编辑中提到的)。
我 运行 遇到了视频工作循环未完成的问题,这是由于音频在视频之前到达 EOS 然后返回 -1
造成的。所以我把原来的 mVideoWorkerThread
改成了下面的:
mVideoWorkerThread = new Thread("VideoThread") {
@Override
public void run() {
if (!Thread.interrupted()) {
try {
if (shouldPushVideo() || audioReachedEOS()) {
workLoopVideo();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
mVideoWorkerThread.start();
private boolean audioReachedEOS() {
return mAudioExtractor.getSampleTime() == -1;
}
所以我使用 audioReachedEOS()
来检查我的音频是否 MediaExtractor
returns -1
。如果是,则表示我的音频已完成,但我的视频尚未完成,因此我会继续我的视频工作循环,直到完成。
这似乎按预期工作(当我只 play/pause 视频而没有搜索时)。 seeking还有个问题,我就不细说了。
我会按原样发布我的应用程序,并在我 运行 遇到问题时更新此答案。