ExoPlayer架构详解与源码分析(8)——Loader

news2024/10/7 16:19:33

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader


文章目录

  • 系列文章目录
  • 前言
  • ProgressiveMediaPeriod
  • Loader
  • ExtractingLoadable
  • BundledExtractorsAdapter
  • Extractor
  • 总结


前言

ProgressiveMediaPeriod的左半部分SampleQueue已经在上篇讲完,相对今天说的这部分还算简单,ProgressiveMediaPeriod右半部分主要为Loader,而Loader中及包含数据的获取也包含数据的解析,本篇主要分析Loader的整体机构和数据解析部分结构。

ProgressiveMediaPeriod

还是先预习下上篇的整体结构,本篇主要分析右半半部分的Loader:
在这里插入图片描述
图中Loader数据的加载主要靠DataSource,而解析部分主要为Executor

Loader

Loader本质上就是就是一个线程池,初始化时就创建了一个ExecutorService,启动时实例化出一个LoadTask放入线程池中执行。

  private final ExecutorService downloadExecutorService;
  public Loader(String threadNameSuffix) {
    this.downloadExecutorService =
        Util.newSingleThreadExecutor(THREAD_NAME_PREFIX + threadNameSuffix);
  }

  public <T extends Loadable> long startLoading(
      T loadable, Callback<T> callback, int defaultMinRetryCount) {
    Looper looper = Assertions.checkStateNotNull(Looper.myLooper());//获取当前启动线程的looper
    fatalError = null;
    long startTimeMs = SystemClock.elapsedRealtime();
    new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
    return startTimeMs;
  }

LoadTask初始化时会传入当前线程的looper和callback,通过looper将后台线程的信息传递到启动线程的callback中执行,所以startLoading的线程必须要包含一个looper,callback也将在启动现场上调用。通常情况下启动线程就是内部播放线程,具体参照之前将的线程模型。

//@LoadTask.java
@Override
    public void run() {
      try {
        boolean shouldLoad;
        synchronized (this) {
          shouldLoad = !canceled;
          executorThread = Thread.currentThread();
        }
        if (shouldLoad) {
          TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
          try {
            loadable.load();//执行loadable
          } finally {
            TraceUtil.endSection();
          }
        }
        synchronized (this) {
          executorThread = null;
          // Clear the interrupted flag if set, to avoid it leaking into a subsequent task.
          Thread.interrupted();
        }
        if (!released) {
          sendEmptyMessage(MSG_FINISH);//将执行结果通过handler发给启动线程looper
        }
     ...
    }


    @Override
    public void handleMessage(Message msg) {
      if (released) {
        return;
      }
      if (msg.what == MSG_START) {
        execute();
        return;
      }
      if (msg.what == MSG_FATAL_ERROR) {
        throw (Error) msg.obj;
      }
      finish();
      long nowMs = SystemClock.elapsedRealtime();
      long durationMs = nowMs - startTimeMs;
      Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
      if (canceled) {
        callback.onLoadCanceled(loadable, nowMs, durationMs, false);
        return;
      }
      switch (msg.what) {
        case MSG_FINISH:
          try {
            callback.onLoadCompleted(loadable, nowMs, durationMs);//执行完毕,在启动线程上调用callback
       ...
      }
    }

可以看到最终是调用了loadable.load方法
这个loadable定义在ProgressiveMediaPeriod的ExtractingLoadable中

ExtractingLoadable

ExtractingLoadable主要包含2个东西DataSource和ProgressiveMediaExtractor,DataSource负责从媒体数据源获取数据,而ProgressiveMediaExtractor则负责将获取的数据解析出Format和Metadata等sample到SampleQueue中,由此完成数据的加载
看下最核心的load方法。

    @Override
    public void load() throws IOException {
      int result = Extractor.RESULT_CONTINUE;
      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {//循环多次读取和sample数据
        try {
          long position = positionHolder.position;//获取当前的读取位置
          dataSpec = buildDataSpec(position);//构建dataSource的dataSpec
          long length = dataSource.open(dataSpec);//打开读取的流,返回实际数据的长度
          if (length != C.LENGTH_UNSET) {
            length += position;//获取加载后的长度
            onLengthKnown();
          }
          ...
          progressiveMediaExtractor.init(//初始化ProgressiveMediaExtractor
              extractorDataSource,//传入dataSource,此时的dataSource已经open可以直接通过调用dataSource的read获取数据
              uri,
              dataSource.getResponseHeaders(),
              position,//当前读取位置
              length,//流加载后的总长度
              extractorOutput);//传入output,最终会关联输出到SampleQueue中

          ...
          while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
            try {
            //当未open时阻塞当前执行,主要作用是当上层正在决定是否要继续加载时阻塞住下一次加载,
            //当上层决定完可以继续加载时loadCondition.open()
            //这里的上层决定一般由LoadControl做出,后面会讲到
              loadCondition.block();
            } catch (InterruptedException e) {
            //如果不继续加载线程会等待,直到调用Loader的cancel方法会executorThread.interrupt();结束当前线程
              throw new InterruptedIOException();
            }
            result = progressiveMediaExtractor.read(positionHolder);//读取并解析数据到,传入positionHolder用于更新下一次读取位置
            long currentInputPosition = progressiveMediaExtractor.getCurrentInputPosition();//获取当前已经读取到的位置
            //如果当前进度达到必要去继续加载检查的阈值时调用上层加载检查判断是否下次加载,这个阈值有个默认值为1024*1024
            if (currentInputPosition > position + continueLoadingCheckIntervalBytes) {
              position = currentInputPosition;
              loadCondition.close();//阻塞下次加载
              handler.post(onContinueLoadingRequestedRunnable);//通过回调询问上层是否需要继续加载
            }
          }
        } finally {
          if (result == Extractor.RESULT_SEEK) {//如果解析器返回RESULT_SEEK
            result = Extractor.RESULT_CONTINUE;//就会再次open数据源,此时的Uri还是同一个,但是position可能已经在解析器中重新指定
          } else if (progressiveMediaExtractor.getCurrentInputPosition() != C.INDEX_UNSET) {
            positionHolder.position = progressiveMediaExtractor.getCurrentInputPosition();
          }
          DataSourceUtil.closeQuietly(dataSource);//关闭当前流
        }
      }
    }

这里有2个While循环,第一个While循环说明同一个数据源可能会从不同的位置被打开多次,当内循环中解析数据需要SEEK跳过一段数据时就会,返回RESULT_SEEK,这个时候跳出内循环,再次执行外循环。下面会讲到什么时候需要SEEK数据,另外我们注意到Loader从打开数据加载操作1M(默认值)数据时就会阻塞住内循环,停止向SampleQueue中写入数据,而是向上层讯问是否需要继续加载数据,如果上层决定继续加载更多数据,就会调用ProgressiveMediaPeriod continueLoading解开阻塞的锁继续加载。因为我们知道数据是加载到内存中的,如果无限制的加载肯定是不行的,需要有节制的加载和释放数据。
接下看下ProgressiveMediaExtractor的实现BundledExtractorsAdapter。

BundledExtractorsAdapter

这个类相当于包裹了一个Extractor,最终读取工作其实是转发给Extractor,初始化时确认当前流对应的Extractor

  @Override
  public void init(
      DataReader dataReader,
      Uri uri,
      Map<String, List<String>> responseHeaders,
      long position,
      long length,
      ExtractorOutput output)
      throws IOException {
      //包装输入流
    ExtractorInput extractorInput = new DefaultExtractorInput(dataReader, position, length);
    this.extractorInput = extractorInput;
    if (extractor != null) {
      return;
    }
    //extractorsFactory通过网络请求的返回的Content-Type和文件的后缀名获取按优先级创建一个Extractor列表
    Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders);
    if (extractors.length == 1) {
      this.extractor = extractors[0];
    } else {
      for (Extractor extractor : extractors) {
        try {
          //通过调用extractor方法获取流的头部信息最终确定当前流是否可以用这个extractor来解析,如TS就是通过判断0x47同步位来确定的
          if (extractor.sniff(extractorInput)) {
            this.extractor = extractor;
            break;
          }
        } catch (EOFException e) {
          // Do nothing.
        } finally {
          Assertions.checkState(this.extractor != null || extractorInput.getPosition() == position);
          extractorInput.resetPeekPosition();
        }
      }
      if (extractor == null) {//没有支持解析的extractor报错
        throw new UnrecognizedInputFormatException(
            "None of the available extractors ("
                + Util.getCommaDelimitedSimpleClassNames(extractors)
                + ") could read the stream.",
            Assertions.checkNotNull(uri));
      }
    }
    extractor.init(output);//初始化extractor
  }

  @Override
  public int read(PositionHolder positionHolder) throws IOException {
    return Assertions.checkNotNull(extractor)
        .read(Assertions.checkNotNull(extractorInput), positionHolder);//将数据的读取解析转发给extractor
  }

Extractor

主要将媒体数据从容器格式中解析出来,支持很多容器格式,每种都做了实现,有很多下图只列了一部分
在这里插入图片描述
FileTypes类里定义了这些格式,inferFileTypeFromUri可以看出他们与文件后缀之间的对应关系

  public static @FileTypes.Type int inferFileTypeFromUri(Uri uri) {
    @Nullable String filename = uri.getLastPathSegment();
    if (filename == null) {
      return FileTypes.UNKNOWN;
    } else if (filename.endsWith(EXTENSION_AC3) || filename.endsWith(EXTENSION_EC3)) {
      return FileTypes.AC3;
    } else if (filename.endsWith(EXTENSION_AC4)) {
      return FileTypes.AC4;
    } else if (filename.endsWith(EXTENSION_ADTS) || filename.endsWith(EXTENSION_AAC)) {
      return FileTypes.ADTS;
    } else if (filename.endsWith(EXTENSION_AMR)) {
      return FileTypes.AMR;
    } else if (filename.endsWith(EXTENSION_FLAC)) {
      return FileTypes.FLAC;
    } else if (filename.endsWith(EXTENSION_FLV)) {
      return FileTypes.FLV;
    } else if (filename.endsWith(EXTENSION_MID)
        || filename.endsWith(EXTENSION_MIDI)
        || filename.endsWith(EXTENSION_SMF)) {
      return FileTypes.MIDI;
    } else if (filename.startsWith(
            EXTENSION_PREFIX_MK,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1))
        || filename.endsWith(EXTENSION_WEBM)) {
      return FileTypes.MATROSKA;
    } else if (filename.endsWith(EXTENSION_MP3)) {
      return FileTypes.MP3;
    } else if (filename.endsWith(EXTENSION_MP4)
        || filename.startsWith(
            EXTENSION_PREFIX_M4,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_M4.length() + 1))
        || filename.startsWith(
            EXTENSION_PREFIX_MP4,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_MP4.length() + 1))
        || filename.startsWith(
            EXTENSION_PREFIX_CMF,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_CMF.length() + 1))) {
      return FileTypes.MP4;
    } else if (filename.startsWith(
            EXTENSION_PREFIX_OG,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_OG.length() + 1))
        || filename.endsWith(EXTENSION_OPUS)) {
      return FileTypes.OGG;
    } else if (filename.endsWith(EXTENSION_PS)
        || filename.endsWith(EXTENSION_MPEG)
        || filename.endsWith(EXTENSION_MPG)
        || filename.endsWith(EXTENSION_M2P)) {
      return FileTypes.PS;
    } else if (filename.endsWith(EXTENSION_TS)
        || filename.startsWith(
            EXTENSION_PREFIX_TS,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_TS.length() + 1))) {
      return FileTypes.TS;
    } else if (filename.endsWith(EXTENSION_WAV) || filename.endsWith(EXTENSION_WAVE)) {
      return FileTypes.WAV;
    } else if (filename.endsWith(EXTENSION_VTT) || filename.endsWith(EXTENSION_WEBVTT)) {
      return FileTypes.WEBVTT;
    } else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
      return FileTypes.JPEG;
    } else if (filename.endsWith(EXTENSION_AVI)) {
      return FileTypes.AVI;
    } else {
      return FileTypes.UNKNOWN;
    }
  }

从这些代码中可以看出EXO所支持的媒体容器格式。
下篇我们以TS容器格式为例,讲解下TsExtractor,了解媒体数据如何从容器格式中解析出来,最终要交给Readerer渲染的。


总结

Loader主要作用就是将加载逻辑放入线程池中管理,然后通过ExtractingLoadable控制数据的加载,而数据主要由DataSource来获取,解析部分主要为Executor,Executor通过持有已经open的DataSource,不断从DataSource中read出数据用来解析媒体,整个过程都只有一个线程,即使到了数据加载部分也是在同一个线程中同步加载的,在这段同步操作中还将加载的控制权交给了上层组件。下篇将会把TsExtractor作为一个典型的解析器,来分析解析器的具体作用和执行过程。


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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

推荐一个Node.js多版本管理的可视化工具

关于Node.js的开发者来说&#xff0c;在开发机器上管理多个不同版本的Node.js是一个常见痛点。之前在开发者安全大全专栏中&#xff0c;提到过解决方法&#xff1a;使用nvm&#xff0c;如果对于nvm还不了解的话&#xff0c;可以前往了解。 对于TJ来说&#xff0c;因为习惯敲命…

优雅写代码之《项目规范》-附加树状图生成

阿丹&#xff1a; 最近有一些小伙伴在跳槽之后接触到了新的项目小组&#xff0c;在讨论如何整理出漂亮的项目结构以及代码书写的时候&#xff0c;既然有小伙伴发问了&#xff0c;那当然就要一起学习&#xff0c;来&#xff01;开卷&#xff01;本文章只作为一个分享&#xff0c…

nvm下载安装以及配置

1. nvm下载 nvm各版本下载链接&#xff1a;Releases coreybutler/nvm-windows GitHub 建议下载安装版的&#xff0c;非安装版还需要额外配置环境变量。 2. nvm安装 注意&#xff1a;在安装 NVM for Windows 之前卸载任何现有版本的 Node.js&#xff08;否则你会遇到版本冲突…

性能测试 —— Jmeter接口处理不低于200次/秒-场景

需求&#xff1a;期望某个接口系统的处理能力不低于200次/秒&#xff0c;如何设计&#xff1f; ①这个场景是看服务器对某个接口的TPS值是否能大于等于200&#xff0c;就可以了&#xff1b; ②系统处理能力&#xff1a;说的就是我们性能测试中的TPS&#xff1b; ③只要设计一…

电子电机行业万界星空科技MES解决方案

现在电子电机行业规模越来越大&#xff0c;也伴随着生产和管理成本走向变高的现象。针对这个问题&#xff0c;mes系统就成为各电子电机制造业的最优选择。 电子机电行业MES涵盖了从原材料采购到最终产品交付的整个过程&#xff0c;包括生产计划、物料管理、生产过程监控、质量…

【漏洞复现】浙大恩特客户资源管理系统 fileupload.jsp 任意文件上传漏洞

文章目录 前言声明一、系统概述二、漏洞描述三、资产探测四、漏洞复现五、修复建议 前言 杭州恩软信息技术有限公司客户资源管理系统fileupload.jsp接口存在安全漏洞&#xff0c;攻击者可通过上传恶意脚本应用&#xff0c;获取服务器控制权限。 声明 请勿利用文章内的相关技术…

Docker之安装mysql主从复制

安装mysql主从复制 1、新建主服务器容器实例3307 docker run -p 3307:3306 --name mysql-master \ -v /mydata/mysql-master/log:/var/log/mysql \ -v /mydata/mysql-master/data:/var/lib/mysql \ -v /mydata/mysql-master/conf:/etc/mysql \ -e MYSQL_ROOT_PASSWORDroot \…

通过cpolar实现外网ssh远程连接linux

现在我有个想法&#xff0c;就是希望通过外网能够远程连接到我的开发板。这里我们就需要使用到一种技术&#xff0c;内网穿透。 内网穿透是一种将内部网络中的设备通过外网进行访问的技术。在linux系统中&#xff0c;实现内网穿透有多种方式&#xff0c;其中最常见的方法是使用…

Spring全家桶源码解析--2.6 Spring scope 限制bean的作用范围

文章目录 前言一、Scope是什么&#xff1f;二、Scope使用2.1 单例&#xff1a;2.1.1 单例Bean的特点如下&#xff1a;2.1.2 单例设计模式 与单例bean&#xff1a; 2.2 原型bean&#xff1a;2.2.1 原型Bean的特点&#xff1a;2.2.2 原型Bean的销毁&#xff1a; 2.3 Request bean…

什么是集成测试?集成的方法有哪些?

前言 综合测试整合测试非常复杂&#xff0c;需要一些开发和逻辑技能。的确如此&#xff01;那么把这个测试整合到我们的测试策略中的目的是什么呢&#xff1f;这个问题我们先不着急回答&#xff0c;让我们一步步往下看你就知道了。 为什么要进行集成测试&#xff1f; 以下是一…

Linux项目自动化构建工具---make/Makefile

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 坚持才是硬道理&#xff01; 一、工具背景 1.会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备完成大型工程的能力。 2.一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分…

达梦集群搭建

一、数据库安装 ###&#xff08;一&#xff09;安装前准备 版本准备 [rootlocalhost ~]# uname -a Linux localhost.localdomain 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux将镜像文件传到/opt目录下 [rootlocalhost100 …

【PyQt小知识 - 1】:QLineEdit内容的更新和获取、设置无边框

文章目录 QLineEdit更新和获取文本框内容设置为无边框 QLineEdit 更新和获取文本框内容 更新&#xff1a;QLineEdit().setText(text) 获取&#xff1a;QLineEdit().text() from PyQt5.QtWidgets import * import sysapp QApplication(sys.argv)window QWidget() window.re…

sqli-labs(Less-4) extractvalue闯关

extractvalue() - Xpath类型函数 1. 确认注入点如何闭合的方式 2. 爆出当前数据库的库名 http://127.0.0.1/sqlilabs/Less-4/?id1") and extractvalue(1,concat(~,(select database()))) --3. 爆出当前数据库的表名 http://127.0.0.1/sqlilabs/Less-4/?id1") …

Java map 详解 - 用法、遍历、排序、常用API等

概要&#xff1a; java.util 中的集合类包含 Java 中某些最常用的类。最常用的集合类是 List 和 Map。 Map 提供了一个更通用的元素存储方法。Map 集合类用于存储元素对&#xff08;称作“键”和“值”&#xff09;&#xff0c;其中每个键映射到一个值。 本文主要介绍java m…

金融行业如何数字化转型?_光点科技

金融行业的数字化转型涉及技术创新的引入、客户体验的改善、内部流程的优化、安全和合规性的加强以及员工技能和企业文化的转变。 技术创新 包括云计算、人工智能、大数据分析和区块链技术的采用。云计算增强数据处理的灵活性&#xff0c;AI和机器学习在风险评估和欺诈检测方面…

【技术分享】配置二层远程端口镜像案例

热门IT课程-CSDN博客文章浏览阅读24次。认证课程介绍&#xff1a;华为HCIA试听课程 &#xff1a; 华为HCIA试听课程&#xff1a;华为HCIA试听课程&#xff1a;华为HCIP试听课程&#xff1a;思科CCNA试听课程&#xff1a;思科CCNA试听课程&#xff1a;思科CCNA试听课程&#xff…

Arduino安装 esp32 by Espressif (2.0.11)

安装Arduino IDE 2.2.1 Arduino 中安装 esp32 by Espressif (2.0.11) Arduino 中安装 DS1302 库文件 2022年安装的Arduino是1.8.10版本的&#xff0c;主控芯片是外置 2MB Flash 的 ESP32-C3芯片。ESP-C3-12F-2M。 202206 Arduino软件的安装、配置与程序下载的全过程演示 2023…

plsql查询中文出现乱码

添加环境变量&#xff1a;如下 变量名&#xff1a;NLS_LANG 变量值&#xff1a;SIMPLIFIED CHINESE_CHINA.ZHS16GBK 变量名&#xff1a;TNS_ADMIN 变量值&#xff1a;D:\instantclient_11_2\network\admin 在Path中添加instantclient_11_2存放路径

TOUGH系列软件教程

查看原文>>>全流程TOUGH系列软件实践技术应用 TOUGH系列软件是由美国劳伦斯伯克利实验室开发的&#xff0c;旨在解决非饱和带中地下水、热运移的通用模拟软件。和传统地下水模拟软件Feflow和Modflow不同&#xff0c;TOUGH系列软件采用模块化设计和有限积分差网格剖分…