ExoPlayer播放器剖析(五)ExoPlayer对AudioTrack的操作

关联博客

ExoPlayer播放器剖析(一)进入ExoPlayer的世界
ExoPlayer播放器剖析(二)编写exoplayer的demo
ExoPlayer播放器剖析(三)流程分析—从build到prepare看ExoPlayer的创建流程
ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec
ExoPlayer播放器剖析(五)ExoPlayer对AudioTrack的操作
ExoPlayer播放器剖析(六)ExoPlayer同步机制分析
ExoPlayer播放器剖析(七)ExoPlayer对音频时间戳的处理
ExoPlayer播放器扩展(一)DASH流与HLS流简介

一、引言:
上一篇博客中,分析了doSomeWork中的renderer.render接口和startRenderers方法,其中,在介绍后者的时候有提过对音频的操作实际上调用了AudioTrack的play()方法进行播放,这篇博客着重介绍下ExoPlayer如何创建AudioTrack并往audiotrack中写数据的。

二、drainOutputBuffer函数分析:
对audiotrack的操作是在renderer.render中完成的,回到render方法, 我们曾贴出过下面的代码片段:

  @Override
  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
   
    /* 是否处理EOS */
    if (pendingOutputEndOfStream) {
   
      pendingOutputEndOfStream = false;
      processEndOfStream();
    }
    ...
    // We have a format.
    /* 配置codec */
    maybeInitCodecOrBypass();
	...
    TraceUtil.beginSection("drainAndFeed");
    /* 消耗解码后的数据 */
    while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
        && shouldContinueRendering(renderStartTimeMs)) {
   }
    /* 填充源数据 */
    while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {
   }
    TraceUtil.endSection();
		...
  }

接下来,看下drainOutputBuffer函数:

  private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
      throws ExoPlaybackException {
   
    /* 判断是否有可用的输出buffer */
    if (!hasOutputBuffer()) {
   
      int outputIndex;
      if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
   
        try {
   
          outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo);
        } catch (IllegalStateException e) {
   
          processEndOfStream();
          if (outputStreamEnded) {
   
            // Release the codec, as it's in an error state.
            releaseCodec();
          }
          return false;
        }
      } else {
   
        /* 从decoder出列一个可用的buffer index */
        outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo);
      }

      if (outputIndex < 0) {
   
        if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {
   
          processOutputMediaFormatChanged();
          return true;
        } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) {
   
          processOutputBuffersChanged();
          return true;
        }
        /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */
        if (codecNeedsEosPropagation
            && (inputStreamEnded || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM)) {
   
          processEndOfStream();
        }
        return false;
      }

      // We've dequeued a buffer.
      if (shouldSkipAdaptationWorkaroundOutputBuffer) {
   
        shouldSkipAdaptationWorkaroundOutputBuffer = false;
        codec.releaseOutputBuffer(outputIndex, false);
        return true;
      } else if (outputBufferInfo.size == 0
          && (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   
        // The dequeued buffer indicates the end of the stream. Process it immediately.
        processEndOfStream();
        return false;
      }
      /* 根据index找到对应的输出buffer */
      this.outputIndex = outputIndex;
      outputBuffer = getOutputBuffer(outputIndex);

      // The dequeued buffer is a media buffer. Do some initial setup.
      // It will be processed by calling processOutputBuffer (possibly multiple times).
      if (outputBuffer != null) {
   
        outputBuffer.position(outputBufferInfo.offset);
        outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
      }
      isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs);
      isLastOutputBuffer =
          lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs;
      updateOutputFormatForTime(outputBufferInfo.presentationTimeUs);
    }

    boolean processedOutputBuffer;
    if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
   
      try {
   
        processedOutputBuffer =
            processOutputBuffer(
                positionUs,
                elapsedRealtimeUs,
                codec,
                outputBuffer,
                outputIndex,
                outputBufferInfo.flags,
                /* sampleCount= */ 1,
                outputBufferInfo.presentationTimeUs,
                isDecodeOnlyOutputBuffer,
                isLastOutputBuffer,
                outputFormat);
      } catch (IllegalStateException e) {
   
        processEndOfStream();
        if (outputStreamEnded) {
   
          // Release the codec, as it's in an error state.
          releaseCodec();
        }
        return false;
      }
    } else {
   
      /* 处理输出buffer */
      processedOutputBuffer =
          processOutputBuffer(
              positionUs,
              elapsedRealtimeUs,
              codec,
              outputBuffer,
              outputIndex,
              outputBufferInfo.flags,
              /* sampleCount= */ 1,
              outputBufferInfo.presentationTimeUs,
              isDecodeOnlyOutputBuffer,
              isLastOutputBuffer,
              outputFormat);
    }

    /* 处理输出buffer结果为true,表示该outputbuffer已经被消耗完,若为false,表示该输出buffer还有残留数据 */
    if (processedOutputBuffer) {
   
      onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
      boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
      /* 将outputBuffer置为空,且buffer的index置为-1,以便下次进入该函数去获取新的输出buffer */
      resetOutputBuffer();
      /* 判断是否需要处理EOS */
      if (!isEndOfStream) {
   
        return true;
      }
      processEndOfStream();
    }

    return false;
  }

整个函数看起来很长,实际上做的主要事情就两件,先确认是否有可用的输出buffer,如果没有,则通过调用MediaCodec的api拿到可用buffer的index,然后通过index找到对应的buffer,在拿到可用的buffer之后,调用processOutputBuffer去处理输出buffer。先看下确定是否有可用buffer的判断条件:

  private boolean hasOutputBuffer() {
   
    return outputIndex >= 0;
  }

因为outputIndex是通过调用MediaCodec.dequeueOutputBufferIndex()接口获得的,如果该值大于等于0,表明现在decoder里面已经解码出来了待播放的数据。接下来就会调用processOutputBuffer函数进行输出buffer的处理了,先说下对于处理结果的分析(重要),exoplayer使用了一个bool变量processedOutputBuffer来记录最终的处理结果,如果该值为true,代表该buffer中的所有数据全部写入到了audiotrack当中,那么就会对输出buffer做一个reset操作(将MediaCodecRender维护的outputBuffer置为null,同时将outputIndex置为-1),这样做的目的是下一次进入该函数时会重新发起MediaCodec.dequeueOutputBufferIndex()的操作去找一个新的可用buffer来继续往audiotrack里面送了,那要是processedOutputBuffer的处理结果是false呢?看下最开头的if就知道,下一次进来将不会去查询新的可用buffer,而是直接将本次buffer中的剩余数据继续送到audiotrack。那么这里就还剩最后一个问题,drainOutputBuffer的这个返回值对外层函数的影响到底是什么?我们看下renderer.render函数中调用处:

### LlamaIndex 多模态 RAG 实现 LlamaIndex 支持多种数据类型的接入与处理,这使得它成为构建多模态检索增强生成(RAG)系统的理想选择[^1]。为了实现这一目标,LlamaIndex 结合了不同种类的数据连接器、索引机制以及强大的查询引擎。 #### 数据连接器支持多样化输入源 对于多模态数据的支持始于数据收集阶段。LlamaIndex 的数据连接器可以从多个异构资源中提取信息,包括但不限于APIs、PDF文档、SQL数据库等。这意味着无论是文本还是多媒体文件中的内容都可以被纳入到后续的分析流程之中。 #### 统一化的中间表示形式 一旦获取到了原始资料之后,下一步就是创建统一而高效的内部表达方式——即所谓的“中间表示”。这种转换不仅简化了下游任务的操作难度,同时也提高了整个系统的性能表现。尤其当面对复杂场景下的混合型数据集时,良好的设计尤为关键。 #### 查询引擎助力跨媒体理解能力 借助于内置的强大搜索引擎组件,用户可以通过自然语言提问的形式轻松获得所需答案;而对于更复杂的交互需求,则提供了专门定制版聊天机器人服务作为补充选项之一。更重要的是,在这里实现了真正的语义级关联匹配逻辑,从而让计算机具备了一定程度上的‘认知’功能去理解和回应人类意图背后所蕴含的意义所在。 #### 应用实例展示 考虑到实际应用场景的需求多样性,下面给出一段Python代码示例来说明如何利用LlamaIndex搭建一个多模态RAG系统: ```python from llama_index import GPTSimpleVectorIndex, SimpleDirectoryReader, LLMPredictor, PromptHelper, ServiceContext from langchain.llms.base import BaseLLM import os def create_multi_modal_rag_system(): documents = SimpleDirectoryReader(input_dir='./data').load_data() llm_predictor = LLMPredictor(llm=BaseLLM()) # 假设已经定义好了具体的大型预训练模型 service_context = ServiceContext.from_defaults( chunk_size_limit=None, prompt_helper=PromptHelper(max_input_size=-1), llm_predictor=llm_predictor ) index = GPTSimpleVectorIndex(documents, service_context=service_context) query_engine = index.as_query_engine(similarity_top_k=2) response = query_engine.query("请描述一下图片里的人物表情特征") print(response) ``` 此段脚本展示了从加载本地目录下各类格式文件开始直到最终完成一次基于相似度排序后的top-k条目返回全过程。值得注意的是,“query”方法接收字符串参数代表使用者想要询问的内容,而在后台则会自动调用相应的解析模块并结合先前准备好的知识库来进行推理计算得出结论。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值