Android ExoPlayer源码分析

关键的类和接口

UI层

  • PlayerView
    源码中的注释:A high level view for Player media playbacks. It displays video, subtitles and album art during playback, and displays playback controls using a PlayerControlView.
    最外层的播放视图。通过自定义exo_simple_player_view.xml可以定制自己的UI
  • PlayerControlView:
    A view for controlling Player instances.
    控制媒体播放的视图。提供exo_playback_control_view.xml可以定制UI.

Note: 自定义UI文件名和视图id需要和library中提供的保持一致

Media层

  • Player: 媒体播放接口. 子类ExoPlayer is an extensible media player that plays MediaSources. 其中SimpleExoPlayer和应用层关系比较近,ExoPlayerImplInternal偏向底层, 用来协调控制media的处理和播放
  • MediaSource: 最开始初始化player的时候通过MediaSource创建媒体源。通过MediaSource创建MediaPeriod
  • MediaPeriod: 加载media数据, 一般在内部通过Loadable的load方法开始读取media data
  • TrackOutput: 接收Extractor提取的数据.
  • Renderer: 渲染从SampleStream读取的media数据
  • ExtractorInput: Provides data to be consumed by an Extractor
  • Extractor: 提取media数据
  • SampleStream: media sample stream
  • DataSource: 真正的数据源

Media处理流程

项目中通过ConcatenatingMediaSource + ProgressiveMediaSource播放playlist,媒体源是服务端的mp3格式的音频文件,暂时只研究了这一部分的音频播放逻辑。

根据项目中的实际应用大致画了一下流程图如下:

采样流程

媒体采样流程图

采样流程:ProgressiveMediaPeriod在startLoading方法中会通过Loader对象在后台线程执行内部类ExtractingLoadable.load方法开始加载media data。 然后交由extractor.read()处理,最终由trackOutput(SampleQueue)真正开始从ExtractorInput中采样。采样的media data会保存到SampleDataQueue中,SampleDataQueue相当于是一个容器。

播放流程

在这里插入图片描述

播放流程:入口在ExoPlayerImplInternal的doSomeWork()方法, 通过Renderer(MediaCodecAudioRenderer)对象render方法渲染音频。关键的就是drainOutputBuffer()方法和feedInputBuffer()方法。drainOutputBuffer取出buffer数据通过processOutputBuffer()写到AudioTrack。 feedInputBuffer则会读取采样流程中SampleDataQueue中的数据保存buffer中。doSomeWork方法会定期执行,从而实现音频播放

代码分析

还是将采样和播放流程分开,代码只截取了关键部分,重要的地方自己加了注释

采样代码入口ProgressiveMediaPeriod.startLoading():

private void startLoading() {
    // ExtractingLoadable是ProgressiveMediaPeriod的内部类
    ExtractingLoadable loadable =
        new ExtractingLoadable(
            uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition);
    ...
    // 将loadable交由loader对象的后台线程执行
    long elapsedRealtimeMs = loader.startLoading(loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));
    ...
  }

接下来就会在后台线程执行ExtractingLoadable任务

// 这里是load方法开始加载数据
public void load() throws IOException, InterruptedException {
      int result = Extractor.RESULT_CONTINUE;
      ExtractorInput input = null;
      // 封装ExtractorInput,持有了一个dataSource(底层是DefaultHttpDataSource)对象,真正从网络上获取数据由DefaultHttpDataSource实现
      DataSource extractorDataSource = dataSource;
      input = new DefaultExtractorInput(extractorDataSource, position, length);
    
      // 选择具体的Extractor并初始化,项目中对应的就是Mp3Extractor
      Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);
      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
        loadCondition.block();
        // extractor开始读取input数据
        result = extractor.read(input, positionHolder);
     }
     ...
 }

// 选择一个具体的Extractor
public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, Uri uri) throws IOException, InterruptedException {
    ...
   for (Extractor extractor : extractors) {
      if (extractor.sniff(input)) {
         this.extractor = extractor;
         break;
      }
  }
  // 初始化extractor,这里的output就是ExtractingLoadable引用的外层的ProgressiveMediaPeriod对象,
  // extractor会通过output得到一个trackOutput也就是SampleQueue对象,所以extractor和ProgressiveMediaPeriod都持有同一个SampleQueue对象
  extractor.init(output);
  return extractor;
}

Mp3Extractor.readSample()开始采样数据:

private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
    ...
    // extractorInput最后会交由trackOutput真正开始采样
    int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
    ...
}

这里的trackOutput也就是SampleQueue对象, 它在Mp3Extractor.init()方法中初始化也就是上面selectExtractor方法中被调用的:

 @Override
  public void init(ExtractorOutput output) {
    extractorOutput = output;
    // extractorOutput就是ProgressiveMediaPeriod
    trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
    extractorOutput.endTracks();
}

后面就会由SampleQueue读取extractorInput数据, SampleQueue也只是一个外层的wrapper容器,真正保存数据的是SampleDataQueue,下面是SampleDataQueue sampleData代码:

public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException {
    length = preAppend(length);
    // 读取sample media data
    int bytesAppended =
        input.read(
            writeAllocationNode.allocation.data,
            writeAllocationNode.translateOffset(totalBytesWritten),
            length);
    ...
    postAppend(bytesAppended);
    return bytesAppended;
}

这里是真正采样media data的地方。input是在ExtractingLoadable.load中创建的DefaultExtractorInput对象,它持有dataSource的引用。我们是需要网络上的数据,所以最终的dataSource是由DefaultHttpDataSourceFactory创建的DefaultHttpDataSource对象。通过input读取数据的时候实际就是由DefaultHttpDataSource建立网络链接读取网络数据,这一部分就不贴代码了。

接下来是播放流程的代码,入口是ExoPlayerImplInternaldoSomeWork方法,这个方法会周期性地执行,应该算是最重要的一个方法。方法内调用renderer.render开始渲染media data. Renderer是一个接口,我们这里需要的是它的实现类MediaCodecAudioRenderer。MediaCodecAudioRenderer没有重写render方法,下面是父类MediaCodecRenderer render方法的代码:

@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
    ...
    if (codec != null) {
      
      // 将buffer数据写到AudioTrack
      while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
      // 从SampleQueue中读取sample data到buffer
      while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}
      TraceUtil.endSection();
    } 
   ...
}

drainOutputBuffer方法内会通过processOutputBuffer方法将buffer数据传递给子类(这里就是MediaCodecAudioRenderer)处理,然后会通过DefaultAudioSink handle,最终会将数据写到AudioTrack中去,音频由AudioTrack播放,代码如下:

// 这里是MediaCodecRenderer方法
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\
	...
    // 调用抽象方法processOutputBuffer,由子类处理输出
	processedOutputBuffer = processOutputBuffer(
              positionUs,
              elapsedRealtimeUs,
              codec,
              outputBuffer,
              outputIndex,
              outputBufferInfo.flags,
              outputBufferInfo.presentationTimeUs,
              isDecodeOnlyOutputBuffer,
              isLastOutputBuffer,
              outputFormat);
     ...
}

// 这里是子类MediaCodecAudioRenderer方法,参数太多没贴
protected boolean processOutputBuffer(....) throws ExoPlaybackException {
	...
	// buffer交由audioSink(DefaultAudioSink)处理, audioSink内部会将buffer的media data写到AudioTrack
    if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) {
      codec.releaseOutputBuffer(bufferIndex, false);
      decoderCounters.renderedOutputBufferCount++;
      return true;
    }
	...
}

接下来是feedInputBuffer部分的代码,它从采样流程中用来保存sample data的SampleQueue中读数据到自己的buffer中。下面是相关部分代码:

// 这里是MediaCodecRenderer方法
private boolean feedInputBuffer() throws ExoPlaybackException {
	// 这里调用父类BaseRenderer方法读数据到buffer
    result = readSource(formatHolder, buffer, false);
}

// 这里是BaseRenderer方法
protected final int readSource(
      FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {
    // 通过stream对象读取sample data到buffer
    int result = stream.readData(formatHolder, buffer, formatRequired);
    ...
}

这里的stream是SampleStream接口,实际是ProgressiveMediaPeriod内部类SampleStreamImpl对象。它是在enable renderer的时候传入的MediaPeriodHolder持有的SampleStream对象, 而MediaPeriodHolder.sampleStreams保存的是由ProgressiveMediaPeriod创建的内部类SampleStreamImpl对象。 所以通过enable renderer就将SampleStreamImpl对象传给了renderer,renderer就可以通过stream来读sample data, 具体代码就不贴了,感兴趣的可以从ExoPlayerImplInternal.enableRenderers()方法开始看。

接着上面BaseRenderer.readSource流程,他会走到SampleStreamImpl(ProgressiveMediaPeriod内部类) readData中:

// 这是SampleStreamImpl方法
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {
    // 只有一行代码, 调用外层类readData
    return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
}

// 这是ProgressiveMediaPeriod方法
int readData(int sampleQueueIndex, FormatHolder formatHolder,
      DecoderInputBuffer buffer, boolean formatRequired) {
    ... 
    // 这里就是从SampleQueue中读sample data
    int result = sampleQueues[sampleQueueIndex].read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);
    ... 
}

总体的流程就是这些了。 其他不同格式的数据稍微看了几眼,流程基本上大同小异。

总结:音频播放主要是有采样和播放两条线。 有一个common的容器SampleQueue, 采样过程从data source读取sample data保存到SampleQueue,播放过程则是renderer通过SampleStream从SampleQueue中读数据到buffer, 然后将数据给具体的media api消费,比如这里的音频播放则是由AudioTrack消费buffer数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值