VLC-Android音频播放不完整问题踏坑

对于音频播放异常的问题,由于我们可以拿到解码前后的数据,因此还是比视频播放异常的问题好分析一些的。通过dump解封装后的es数据和解码后的pcm数据,我们可以快速的定位问题出现的大致位置:demux,decoder,aout。本题也是一样,先dump es和pcm数据,发现都没有问题,那基本就是aout部分惹的祸了。

通过加日志,发现在音频播放快结束的时候,aout调用了两次flush。全局搜索aout_DecFlush函数,发现有三种情况下会调用:1.decoder flush;2.decoder delete;3.decoder drain。decoder flush一般都是发生在seek操作后,本问题是自然播放到音频结束,所以两次aout_DecFlush应该是先后发生在decoder drain(消耗解码器缓存)和decoder delete(销毁解码器)中,后续通过加日志也印证了我们的猜测。我们看下aout_DecFlush具体做了啥:

//这里aout_DecFlush还有一个布尔参数wait,只有在decoder drain的时候将wait为true

void aout_DecFlush (audio_output_t *aout, bool wait)

{

    aout_owner_t *owner = aout_owner (aout);

    aout_OutputLock (aout);

    owner->sync.end = VLC_TS_INVALID;

    if (owner->mixer_format.i_format)

    {

        //如果wait为true,会先将filter中的block播放完

        if (wait)

        {

            block_t *block = aout_FiltersDrain (owner->filters);

            if (block)

                aout_OutputPlay (aout, block);

        }

        else

            aout_FiltersFlush (owner->filters);

        //flush aout

        aout_OutputFlush (aout, wait);

    }

    aout_OutputUnlock (aout);

}

这里可以看到,无论wait是否为true,最终都会调用aout_OutputFlush。如果wait为true,就先把没播放完的音频播放完后再flush。那么问题来了,aout_OutputPlay真的能保证音频全部播放完成吗?答案显然是否定的,不然也就不会出现问题了,我们看下audiotrack.c的Play函数:

static void

Play( audio_output_t *p_aout, block_t *p_buffer )

{

    JNIEnv *env = NULL;

    size_t i_buffer_offset = 0;

    aout_sys_t *p_sys = p_aout->sys;

    //去掉IEC61937的包头

    if( p_sys->b_passthrough && p_sys->fmt.i_format == VLC_CODEC_SPDIFB

     && ConvertFromIEC61937( p_aout, p_buffer ) != 0 )

    {

        block_Release(p_buffer);

        return;

    }

    vlc_mutex_lock( &p_sys->lock );

    if( p_sys->b_error || !( env = GET_ENV() ) )

        goto bailout;

    if( p_sys->i_chans_to_reorder )

       aout_ChannelReorder( p_buffer->p_buffer, p_buffer->i_buffer,

                            p_sys->i_chans_to_reorder, p_sys->p_chan_table,

                            p_sys->fmt.i_format );

    //将数据写到RingBuffer中

    while( i_buffer_offset < p_buffer->i_buffer && !p_sys->b_error )

    {

        size_t i_circular_free;

        size_t i_data_offset;

        size_t i_data_size;

        /* Wait for enough room in circular buffer */

        while( !p_sys->b_error && ( i_circular_free = p_sys->circular.i_size -

               ( p_sys->circular.i_write - p_sys->circular.i_read ) ) == 0 )

            vlc_cond_wait( &p_sys->aout_cond, &p_sys->lock );

        if( p_sys->b_error )

            goto bailout;

        i_data_offset = p_sys->circular.i_write % p_sys->circular.i_size;

        i_data_size = __MIN( p_buffer->i_buffer - i_buffer_offset,

                             p_sys->circular.i_size - i_data_offset );

        i_data_size = __MIN( i_data_size, i_circular_free );

        switch( p_sys->i_write_type )

        {

        case WRITE_BYTEARRAY:

        case WRITE_BYTEARRAYV23:

            (*env)->SetByteArrayRegion( env, p_sys->circular.u.p_bytearray,

                                        i_data_offset, i_data_size,

                                        (jbyte *)p_buffer->p_buffer

                                        + i_buffer_offset);

            break;

        case WRITE_SHORTARRAYV23:

            i_data_offset &= ~1;

            i_data_size &= ~1;

            (*env)->SetShortArrayRegion( env, p_sys->circular.u.p_shortarray,

                                         i_data_offset / 2, i_data_size / 2,

                                         (jshort *)p_buffer->p_buffer

                                         + i_buffer_offset / 2);

            break;

        case WRITE_FLOATARRAY:

            i_data_offset &= ~3;

            i_data_size &= ~3;

            (*env)->SetFloatArrayRegion( env, p_sys->circular.u.p_floatarray,

                                         i_data_offset / 4, i_data_size / 4,

                                         (jfloat *)p_buffer->p_buffer

                                         + i_buffer_offset / 4);

            break;

        case WRITE_BYTEBUFFER:

            memcpy( p_sys->circular.u.bytebuffer.p_data + i_data_offset,

                    p_buffer->p_buffer + i_buffer_offset, i_data_size );

            break;

        }

        i_buffer_offset += i_data_size;

        p_sys->circular.i_write += i_data_size;

        if( !p_sys->b_thread_waiting )

            vlc_cond_signal( &p_sys->thread_cond );

    }

bailout:

    vlc_mutex_unlock( &p_sys->lock );

    block_Release( p_buffer );

}

大家有时间可以详细阅读下audiotrack.c的实现,这里简单介绍下,audiotrack.c中维护了一个RingBuffer,开缓存是为了防止AudioTrack出现underrun。audiotrack.c中维护了两个线程,第一个就是Play函数所在的线程,用来往RingBuffer中写数据;另一个线程为AudioTrack_Thread,它从RingBuffer中取数据并真正的写到AudioTrack中。到这里问题原因就很清楚了,audiotrack.c的Play仅仅将audio数据写到RingBuffer中就返回,并没有真正的播放完,此时flush AudioTrack就会导致最后一部分数据播放不出来。

我始终不理解VLC为什么要在decoder drain部分加上flush这种高风险操作,直到我去查了master线最新的代码:

//最新代码在decoder drain部分改成了使用aout_DecDrain函数,这个函数是新增的,看来之前的flush也被别人诟病了

if( p_block == NULL && p_owner->dec.fmt_in.i_cat == AUDIO_ES )

{   /* Draining: the decoder is drained and all decoded buffers are

     * queued to the output at this point. Now drain the output. */

    if( p_owner->p_aout != NULL )

        aout_DecDrain( p_owner->p_aout );

}

void aout_DecDrain(audio_output_t *aout)

{

    aout_owner_t *owner = aout_owner (aout);

    if (!owner->mixer_format.i_format)

        return;

    if (owner->filters)

    {

        block_t *block = aout_FiltersDrain (owner->filters);

        if (block)

            aout->play(aout, block, vlc_tick_now());

    }

    //相对之前版本的aout_DecFlush函数,其实就多了这一行

    aout_Drain(aout);

    vlc_clock_Reset(owner->sync.clock);

    if (owner->filters)

        aout_FiltersResetClock(owner->filters);

    owner->sync.discontinuity = true;

    owner->original_pts = VLC_TICK_INVALID;

}

static void aout_Drain(audio_output_t *aout)

{

    //如果module实现了drain函数,直接调用

    if (aout->drain)

        aout->drain(aout);

    //如果没实现,调用aout_TimeGet获取aout缓存的音频时长,sleep这么长时间让它播放完

    //可以说是很直接的方式了。。。audiotrack.c目前还没有实现drain函数,所以走的也是这个分支

    else

    {

        vlc_tick_t delay;

        if (aout_TimeGet(aout, &delay) == 0)

            vlc_tick_sleep(delay);

    }

}

把这套方案搬过来,问题完美解决~


版权声明:本文为wangtonghua97原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。