ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod


文章目录

  • 系列文章目录
  • 前言
  • ProgressiveMediaPeriod
  • 总结


前言

中途间隔了一段时间,之前写了那么多铺垫,终于看到ProgressiveMediaPeriod实现部分了

ProgressiveMediaPeriod

有了之前的那些铺垫,这里直接看源码了

  @Override
  public void prepare(Callback callback, long positionUs) {
    this.callback = callback;
    loadCondition.open();//保证继续加载的开关打开,Loader不阻塞
    startLoading();
  }
  
  private void startLoading() {
    ExtractingLoadable loadable =//创建loadable 共Loader加载
        new ExtractingLoadable(//extractorOutput监听,也就是Loader加载过程中会回调ProgressiveMediaPeriod的track,endTracks
            uri, dataSource, progressiveMediaExtractor, /* extractorOutput= */ this, loadCondition);
    if (prepared) {//如果已经准备完成
      Assertions.checkState(isPendingReset());
      if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) {
      //当前定位位置已经超过总时长,直接加载结束
        loadingFinished = true;
        pendingResetPositionUs = C.TIME_UNSET;
        return;
      }
      //通过seekMap查找出当前时间对于的数据位置
      loadable.setLoadPosition(
          checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position,
          pendingResetPositionUs);
      for (SampleQueue sampleQueue : sampleQueues) {
        //将所有的轨道开始时间同步
        sampleQueue.setStartTimeUs(pendingResetPositionUs);
      }
      pendingResetPositionUs = C.TIME_UNSET;
    }
    //获取开始加载时所有轨道已经提前的数据块总数,后面通过当前的数据块总数和开始的数量对比,可以判断出是否加载了新的数据
    extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
    long elapsedRealtimeMs =
        loader.startLoading(//Loader开始加载,具体过程参照Loader部分的文章,此时加载状态的回调this,也就是加载完成后会调用ProgressiveMediaPeriod  onLoadCompleted,seekMap
            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));
    DataSpec dataSpec = loadable.dataSpec;
    //触发监听
    mediaSourceEventDispatcher.loadStarted(
        new LoadEventInfo(loadable.loadTaskId, dataSpec, elapsedRealtimeMs),
        C.DATA_TYPE_MEDIA,
        C.TRACK_TYPE_UNKNOWN,
        /* trackFormat= */ null,
        C.SELECTION_REASON_UNKNOWN,
        /* trackSelectionData= */ null,
        /* mediaStartTimeUs= */ loadable.seekTimeUs,
        durationUs);
  }

  @Override
  //Loader加载时如果解析器需要输出Sample数据会先回调track,获取TrackOutput
  public TrackOutput track(int id, int type) {
    return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false));
  }
  //构建TrackOutput
  private TrackOutput prepareTrackOutput(TrackId id) {
    int trackCount = sampleQueues.length;
    for (int i = 0; i < trackCount; i++) {
      //查询当前的sampleQueue是否已创建直接返回
      if (id.equals(sampleQueueTrackIds[i])) {
        return sampleQueues[i];
      }
    }
    SampleQueue trackOutput =
        //创建新的SampleQueue对应一个新的轨道,这里传入了缓存分配器allocator
        SampleQueue.createWithDrm(allocator, drmSessionManager, drmEventDispatcher);
    trackOutput.setUpstreamFormatChangeListener(this);//设置Format改变的监听
    @NullableType
    TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
    sampleQueueTrackIds[trackCount] = id;
    this.sampleQueueTrackIds = Util.castNonNullTypeArray(sampleQueueTrackIds);
    //更新全局的SampleQueue数组
    @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);
    sampleQueues[trackCount] = trackOutput;
    this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);
    return trackOutput;
  }

  @Override
  //当解析器已经将所有轨道解析出来时会调用此方法,参照H264Reader部分
  public void endTracks() {
    sampleQueuesBuilt = true;
    //当前方法在解析的子线程中回调,需要将方法方法当前ProgressiveMediaPeriod的线程中执行maybeFinishPrepare
    handler.post(maybeFinishPrepareRunnable);
  }
  
  private void maybeFinishPrepare() {
    if (released || prepared || !sampleQueuesBuilt || seekMap == null) {
      return;
    }
    //确保所有轨道均已解析
    for (SampleQueue sampleQueue : sampleQueues) {
      if (sampleQueue.getUpstreamFormat() == null) {
        return;
      }
    }
    //阻塞住loader的解析,防止继续更新sampleQueues
    loadCondition.close();
    //创建TrackGroup,trackState 供后续的selectTracks使用
    int trackCount = sampleQueues.length;
    TrackGroup[] trackArray = new TrackGroup[trackCount];
    boolean[] trackIsAudioVideoFlags = new boolean[trackCount];
    for (int i = 0; i < trackCount; i++) {
      Format trackFormat = checkNotNull(sampleQueues[i].getUpstreamFormat());
      @Nullable String mimeType = trackFormat.sampleMimeType;
      boolean isAudio = MimeTypes.isAudio(mimeType);
      boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);
      trackIsAudioVideoFlags[i] = isAudioVideo;
      haveAudioVideoTracks |= isAudioVideo;
      ...
      trackFormat = trackFormat.copyWithCryptoType(drmSessionManager.getCryptoType(trackFormat));
      trackArray[i] = new TrackGroup(/* id= */ Integer.toString(i), trackFormat);
    }
    trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags);
    prepared = true;//标记准备完成
    checkNotNull(callback).onPrepared(this);//通知上层prepared ,上层接下就会去调用ProgressiveMediaPeriod.selectTracks获取轨道信息
  }
  
  @Override
  //获取轨道
  public long selectTracks(
      @NullableType ExoTrackSelection[] selections,//TrackSelector 部分会说到
      boolean[] mayRetainStreamFlags,
      @NullableType SampleStream[] streams,//Renderer部分会说的
      boolean[] streamResetFlags,
      long positionUs) {
    assertPrepared();//确保已经准备完成
    TrackGroupArray tracks = trackState.tracks;
    boolean[] trackEnabledStates = trackState.trackEnabledStates;
    int oldEnabledTrackCount = enabledTrackCount;
    // 去除原来mayRetainStreamFlags标记的无需保留的轨道
    for (int i = 0; i < selections.length; i++) {
      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
        int track = ((SampleStreamImpl) streams[i]).track;
        Assertions.checkState(trackEnabledStates[track]);
        enabledTrackCount--;
        trackEnabledStates[track] = false;
        streams[i] = null;
      }
    }
    //如果是第一次selectTracks,而且positionUs 位置又不为0,或者之前一次selectTracks禁用了所有的轨道,这2种情况就需要Seek
    boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0;
    // 开始通过selections选择新的轨道
    for (int i = 0; i < selections.length; i++) {
      if (streams[i] == null && selections[i] != null) {
        ExoTrackSelection selection = selections[i];
        Assertions.checkState(selection.length() == 1);
        Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
        int track = tracks.indexOf(selection.getTrackGroup());
        Assertions.checkState(!trackEnabledStates[track]);
        enabledTrackCount++;
        trackEnabledStates[track] = true;
        streams[i] = new SampleStreamImpl(track);//向入参中赋值
        streamResetFlags[i] = true;
        if (!seekRequired) {
          SampleQueue sampleQueue = sampleQueues[track];
          seekRequired =
              !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true)
                  && sampleQueue.getReadIndex() != 0;
        }
      }
    }
    if (enabledTrackCount == 0) {
      pendingDeferredRetry = false;
      notifyDiscontinuity = false;
      if (loader.isLoading()) {
        // Discard as much as we can synchronously.
        for (SampleQueue sampleQueue : sampleQueues) {
          sampleQueue.discardToEnd();
        }
        loader.cancelLoading();
      } else {
        for (SampleQueue sampleQueue : sampleQueues) {
          sampleQueue.reset();
        }
      }
    } else if (seekRequired) {
      positionUs = seekToUs(positionUs);
      // We'll need to reset renderers consuming from all streams due to the seek.
      for (int i = 0; i < streams.length; i++) {
        if (streams[i] != null) {
          streamResetFlags[i] = true;
        }
      }
    }
    seenFirstTrackSelection = true;
    return positionUs;
  }
  @Override
  //解析器解析出SeekMap时回调
  public void seekMap(SeekMap seekMap) {
    handler.post(() -> setSeekMap(seekMap));
  }
  @Override
  public boolean continueLoading(long playbackPositionUs) {
    if (loadingFinished//加载完成,出错等情况返回false
        || loader.hasFatalError()
        || pendingDeferredRetry
        || (prepared && enabledTrackCount == 0)) {
      return false;
    }
    boolean continuedLoading = loadCondition.open();
    if (!loader.isLoading()) {//当前没有正在加载
      startLoading();//再次startLoading
      continuedLoading = true;
    }
    return continuedLoading;
  }
  @Override
  //Loader加载完成后回调
  public void onLoadCompleted(
      ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
    if (durationUs == C.TIME_UNSET && seekMap != null) {//此时durationUs 未知,seekMap 已经有了
      boolean isSeekable = seekMap.isSeekable();
      long largestQueuedTimestampUs =//查询所有轨道中时间戳最大值
          getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true);
      durationUs =//将最大值+10毫秒作为当前媒体的时长
          largestQueuedTimestampUs == Long.MIN_VALUE
              ? 0
              : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
      //此时durationUs 等信息已知可以通过MediaSource更新一把TimeLine
      listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive);
    }
    StatsDataSource dataSource = loadable.dataSource;
    //StatsDataSource可以缓存Uri和ResponseHeader,获取这些值构建LoadEventInfo 
    LoadEventInfo loadEventInfo =
        new LoadEventInfo(
            loadable.loadTaskId,
            loadable.dataSpec,
            dataSource.getLastOpenedUri(),
            dataSource.getLastResponseHeaders(),
            elapsedRealtimeMs,
            loadDurationMs,
            dataSource.getBytesRead());
    loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
    //触发监听
    mediaSourceEventDispatcher.loadCompleted(
        loadEventInfo,
        C.DATA_TYPE_MEDIA,
        C.TRACK_TYPE_UNKNOWN,
        /* trackFormat= */ null,
        C.SELECTION_REASON_UNKNOWN,
        /* trackSelectionData= */ null,
        /* mediaStartTimeUs= */ loadable.seekTimeUs,
        durationUs);
    loadingFinished = true;//此时的loadingFinished 已为true,没设置为false前是无法continueLoading的
    //请求上层继续加载,是否继续加载由上层决定,如果上层同意继续加载会调用ProgressiveMediaPeriod的continueLoading
    checkNotNull(callback).onContinueLoadingRequested(this);
  }
  

这里再总结下ProgressiveMediaPeriod执行过程:

  1. 首先调用prepare方法启动Loader去获取资源的轨道信息,此处参考ExoPlayer架构详解与源码分析(8)——Loader
  2. Loader中的解析器获取到信息后回回调endTrack通知ProgressiveMediaPeriod prepare完成
  3. ProgressiveMediaPeriod 会进一步通知上层此时MeidaSource已经准备完成,上层再Renderer(四大组件之一)绘制前会通过ExoPlayer的另一大组件TrackSelector(后面会说到),调用ProgressiveMediaPeriod 的selectTracks选择轨道数据,也就将SampleQueue中的数据提供出去,此处参考ExoPlayer架构详解与源码分析(7)——SampleQueue
  4. 同时Loader从打开到当前加载数据量超过1M(默认值)时就会阻塞当前的Loader,然后询问上层是否继续加载
  5. 上层主要是通过后面要将LoadControl判断,如果上层决定继续加载更多数据,就会调用ProgressiveMediaPeriod continueLoading解开阻塞的锁继续加载。因为我们知道数据是加载到内存中的,如果无限制的加载肯定是不行的,需要有节制的加载和释放数据。而LoadControl就负责这块工作后面会讲到。
  6. 而上面过程当需要释放已经播放的内存时就会调用discardBuffer方法释放Sample中的内存
  7. 当所有的数据加载完成的时候会调用onLoadCompleted将loadingFinished 标记为true

总结

到这里Exoplayer中最复杂的组件MediaSource就全部分析完毕了,之前说了MediaSource在整个运载火箭中的角色就类似于燃料系统,确保火箭顺利升空,在运行过程中持续稳定的为火箭提供燃料。这些燃料提供给谁了呢,或者说是被谁消耗了呢。这就是下面要讲的火箭的核心发动机Renderers,也是Exoplayer四大组件中另一个重要的角色。


版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/779805.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

时间、查找、打包、行过滤与指令的运行——linux指令学习(二)

前言&#xff1a;本节内容标题虽然为指令&#xff0c;但是并不只是讲指令&#xff0c; 更多的是和指令相关的一些原理性的东西。 如果友友只想要查一查某个指令的用法&#xff0c; 很抱歉&#xff0c; 本节不是那种带有字典性质的文章。但是如果友友是想要来学习的&#xff0c;…

如何确保 PostgreSQL 在高并发写操作场景下的数据完整性?

文章目录 一、理解数据完整性二、高并发写操作带来的挑战三、解决方案&#xff08;一&#xff09;使用合适的事务隔离级别&#xff08;二&#xff09;使用合适的锁机制&#xff08;三&#xff09;处理死锁&#xff08;四&#xff09;使用索引和约束&#xff08;五&#xff09;批…

系统学习ElastricSearch(一)

不知道大家在项目中是否使用过ElastricSearch&#xff1f;大家对它的了解又有多少呢&#xff1f;官网的定义&#xff1a;Elasticsearch是一个分布式、可扩展、近实时的搜索与数据分析引擎。今天我们就来揭开一下它的神秘面纱&#xff08;以下简称ES&#xff09;。 ES 是使用 J…

uniapp零基础入门Vue3组合式API语法版本开发咸虾米壁纸项目实战

嗨&#xff0c;大家好&#xff0c;我是爱搞知识的咸虾米。 今天给大家带来的是零基础入门uniapp&#xff0c;课程采用的是最新的Vue3组合式API版本&#xff0c;22年发布的uniappVue2版本获得了官方推荐&#xff0c;有很多同学等着我这个vue3版本的那&#xff0c;如果没有学过vu…

CH12_函数和事件

第12章&#xff1a;Javascript的函数和事件 本章目标 函数的概念掌握常用的系统函数掌握类型转换掌握Javascript的常用事件 课程回顾 Javascript中的循环有那些&#xff1f;Javascript中的各个循环特点是什么&#xff1f;Javascript中的各个循环语法分别是什么&#xff1f;…

网页封装APP:让您的网站变身移动应用

网页封装APP&#xff1a;让您的网站变身移动应用 随着移动设备的普及&#xff0c;越来越多的人开始使用移动设备浏览网站。但是&#xff0c;传统的网站设计并不适合移动设备的屏幕尺寸和交互方式&#xff0c;这导致了用户体验不佳和流失。 有没有办法让您的网站变身移动应用&…

【ROS2】初级:客户端-编写一个简单的服务和客户端(Python)

目标&#xff1a;使用 Python 创建并运行服务节点和客户端节点。 教程级别&#xff1a;初学者 时间&#xff1a;20 分钟 目录 背景 先决条件 任务 1. 创建一个包2. 编写服务节点3. 编写客户端节点4. 构建并运行 摘要 下一步 相关内容 背景 当节点通过服务进行通信时&#xff0c…

【项目日记(一)】梦幻笔耕-数据层实现

❣博主主页: 33的博客❣ ▶️文章专栏分类:项目日记◀️ &#x1f69a;我的代码仓库: 33的代码仓库&#x1f69a; &#x1faf5;&#x1faf5;&#x1faf5;关注我带你了解更多项目内容 目录 1.前言2.后端模块3数据库设计4.mapper实现4.1UserInfoMapper4.2BlogMapper 5.总结 1.…

机器学习筑基篇,​Ubuntu 24.04 快速安装 PyCharm IDE 工具,无需激活!

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] Ubuntu 24.04 快速安装 PyCharm IDE 工具 描述&#xff1a;虽然在之前我们安装了VScode&#xff0c;但是其对于使用Python来写大型项目以及各类配置还是比较复杂的&#xff0c;所以这里我们还是推…

U盘非安全拔出后的格式化危机与数据拯救策略

在数字化时代&#xff0c;U盘作为便捷的数据携带工具&#xff0c;其重要性不言而喻。然而&#xff0c;许多用户在日常使用中往往忽视了安全退出的重要性&#xff0c;直接拔出U盘后再插入时可能会遭遇“需要格式化”的提示&#xff0c;这一状况不仅令人措手不及&#xff0c;更可…

YOLOv9报错:AttributeError: ‘list‘ object has no attribute ‘view‘

报错信息如下&#xff1a; red_distri, pred_scores torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split( AttributeError: ‘list’ object has no attribute ‘view’ 解决方法&#xff1a; 去yolov9/utils/loss_tal.py把167行代码更改&#…

Android最近任务显示的图片

Android最近任务显示的图片 1、TaskSnapshot截图1.1 snapshotTask1.2 drawAppThemeSnapshot 2、导航栏显示问题3、Recentan按键进入最近任务 1、TaskSnapshot截图 frameworks/base/services/core/java/com/android/server/wm/TaskSnapshotController.java frameworks/base/cor…

Blazor SPA 的本质是什么以及服务器端渲染如何与 Blazor 的新 Web 应用程序配合使用

Blazor 通常被称为单页应用程序 (SPA) 框架。当我第一次开始使用 Blazor 时&#xff0c;我对 SPA 的含义、组件如何为 SPA 架构做出贡献以及所有这些如何与交互性联系在一起感到困惑。 今天&#xff0c;我将解答大家可能关心的三个问题&#xff1a; 什么是 SPA&#xff1f;了…

Sentinel-1 Level 1数据处理的详细算法定义(一)

《Sentinel-1 Level 1数据处理的详细算法定义》文档定义和描述了Sentinel-1实现的Level 1处理算法和方程&#xff0c;以便生成Level 1产品。这些算法适用于Sentinel-1的Stripmap、Interferometric Wide-swath (IW)、Extra-wide-swath (EW)和Wave模式。 今天介绍的内容如下&…

14-42 剑和诗人16 - 如何从一个技术人员到CTO再到投资人的角色转变

​​​​​​ 我清楚地记得我的职业轨迹发生转变的那个关键时刻。当时&#xff0c;我正向整个执行领导团队和董事会成员介绍我们部门的技术路线图&#xff0c;感到说服这些有影响力的利益相关者资助一系列雄心勃勃的计划的压力。我知道他们的支持&#xff08;和资金&#xff09…

英语学习交流小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;每日打卡管理&#xff0c;备忘录管理&#xff0c;学习计划管理&#xff0c;学习资源管理&#xff0c;论坛交流 微信端账号功能包括&#xff1a;系统首页&#xff0c;学习资源&…

基于最大相邻夹角的边缘点提取(matlab)

1、背景介绍 边缘点是指点云数据中代表物体或场景几何形状突变的那些点。在三维点云中&#xff0c;边缘点通常标志着不同表面或物体的分界&#xff0c;或者是物体表面上的不规则性&#xff0c;如裂缝、棱角、突起等。点云边缘检测的作用非常重要&#xff0c;最常见是进行特征点…

应用监控SkyWalking调研

参考&#xff1a; 链路追踪( Skyworking )_skywalking-CSDN博客 企业级监控项目Skywalking详细介绍&#xff0c;来看看呀-CSDN博客 SkyWalking 极简入门 | Apache SkyWalking 使用 SkyWalking 监控 ClickHouse Server | Apache SkyWalking https://zhuanlan.zhihu.com/p/3…

45 mysql truncate 的实现

前言 truncate 是一个我们也经常会使用到的命令 其作用类似于 delete from $table; 但是 他会比 delete 块很多&#xff0c;这里我们来看一下 它的实现 delete 的时候会逐行进行处理, 打上 删除标记, 然后 由后台任务 进行数据处理 truncate table 的实现 执行 sql 如下 …

【测试专题】软件总体计划方案(2024原件word)

测试目标&#xff1a;确保项目的需求分析说明书中的所有功能需求都已实现&#xff0c;且能正常运行&#xff1b;确保项目的业务流程符合用户和产品设计要求&#xff1b;确保项目的界面美观、风格一致、易学习、易操作、易理解。 获取&#xff1a;软件全套文档过去进主页。 一、…