Exoplayer无缝开播优化

本文讨论了如何在Exoplayer中实现无缝开播,通过梳理其加载流程并优化预加载机制,以减少改动原生加载流程带来的复杂性和风险。重点介绍了数据流打开流程和自定义DataSource的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

无缝开播思路

无缝开播的思想是

  1. 利用播放开播之前的冗余时间,提前建立好网络连接,预加载部分数据,
  2. 播放器开播时,首先校验是否有已经提前打开的数据流,如果有的话直接加载渲染预加载的数据流,否则执行默认开播流程

Exoplayer 预加载实现痛点:

Exoplayer有自身的加载流程和缓存机制,机制复杂,如果直接改动原有的加载机制,工作量庞大,稳定性差,不利于线上大规模落地

Exoplayer 优化思路:

梳理Exoplayer数据加载流程,加载的中间环节进行偷梁换柱,是否能够替换成我们自己实现我们已经加载好的数据流模块

Exoplayer 数据流打开流程:

数据流打开入口
ProgressiveMediaPeriod.ExtractingLoadable.load()

public void load() throws IOException {
      int result = Extractor.RESULT_CONTINUE;
      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
        try {
          //构建请求数据
          dataSpec = buildDataSpec(position);
          //打开数据流, 数据流的打开由dataSource 实现, dataSource能否替换成外部已经实现的dataSource
          long length = dataSource.open(dataSpec);
          
        } finally {
          if (result == Extractor.RESULT_SEEK) {
            result = Extractor.RESULT_CONTINUE;
          } else if (progressiveMediaExtractor.getCurrentInputPosition() != C.POSITION_UNSET) {
            positionHolder.position = progressiveMediaExtractor.getCurrentInputPosition();
          }
          DataSourceUtil.closeQuietly(dataSource);
        }
      }
    }

DataSource 创建过程

fun play(url: String) {
        //创建数据流工厂
        var dataSourceFactory = DefaultHttpDataSource.Factory()
        //创建MediaSource
        val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(mediaItem)
        //mediaSource设置给Exoplayer
        player.setMediaSource(mediaSource)
        player.prepare()
        //播放
        player.play()
}
 
public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource {
 
  public static final class Factory implements HttpDataSource.Factory {
 
    @Override
    public DefaultHttpDataSource createDataSource() {
      //创建DataSource, 返回给 ProgressiveMediaPeriod.ExtractingLoadable, 实现数据流的打开, 此处进行偷梁换柱?
      DefaultHttpDataSource dataSource =
          new DefaultHttpDataSource(
              userAgent,
              connectTimeoutMs,
              readTimeoutMs,
              allowCrossProtocolRedirects,
              defaultRequestProperties,
              contentTypePredicate,
              keepPostFor302Redirects);
      if (transferListener != null) {
        dataSource.addTransferListener(transferListener);
      }
      return dataSource;
    }
  }
}

方案实现

自定义CustomDataSource 和CustomDataSource. Factory。 HttpDataSource.Factory. 重写 createDataSource() 方法,实现预加载DataSource返回和降级默认数据加载
方案代码如下:
CustomDataSource

public class CustomDataSource extends DefaultHttpDataSource {
 
    public static final class Factory implements HttpDataSource.Factory {
        public static final String EXO_PRELOAD_KEY = "exo-preload-key";
        public Factory() {
            defaultRequestProperties = new RequestProperties();
            connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
            readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
        }
 
        
        @Override
        public CustomDataSource createDataSource() {
            //获取播放链接,作为预加载的key, 进行预加载DataSource检索
            String url = defaultRequestProperties
                    .getSnapshot()
                    .get(EXO_PRELOAD_KEY);
            if (!TextUtils.isEmpty(url)) {
                CustomDataSource dataSource = ExoPreloadManager.getInstance().getDataSourceByKey(url);
                if (dataSource != null) {
                    //如果存在已经预加载的DataSource, 直接返回预加载的dataSource
                    ExoPreloadManager.getInstance().removeDataSourceByKey(url);
                    return dataSource;
                }
            }
             
            //没有预加载的dataSource, 执行默认数据加载逻辑
            CustomDataSource dataSource = new CustomDataSource(userAgent,
                    connectTimeoutMs,
                    readTimeoutMs,
                    allowCrossProtocolRedirects,
                    defaultRequestProperties);
             
            return dataSource;
        }
    }
 
    
    @Override
    public long open(DataSpec dataSpec) throws HttpDataSourceException {
        Logu.d(TAG, "open isOpened = " + isOpened);
        /* 1. 如果预加载成功,isOpened 为true, 此时需要直接返回,避免再一次打开数据流
         * 2. 如果没有预加载,执行默认打开逻辑
         */
        if (isOpened) {
            return bytesToRead;
        }
        bytesToRead = super.open(dataSpec);
        isOpened = true;
        Logu.d(TAG, "open() after execute super open isOpened = " + isOpened);
        return bytesToRead;
    }
}

ExoPreloadManager

class ExoPreloadManager private constructor() {
 
    private val runningDataSource: ConcurrentHashMap<String, CustomDataSource>
    private val executorService: ExecutorService
 
    fun addDataSource(urlList: List<String>) {
        urlList.forEach { addDataSource(it) }
    }
 
    fun addDataSource(url: String) {
        if (TextUtils.isEmpty(url)
            || runningDataSource.containsKey(url)
        ) {
            return
        }
        executorService.execute {
            //创建预加载DataSource
            createAndOpenDataSource(url)
        }
    }
 
    fun getDataSourceByKey(url: String): CustomDataSource? {
        //提供给CustomDataSource.Factroy.createDataSource() 预加载的dataSource
        return runningDataSource.get(url) as? CustomDataSource
    }
 
    fun removeDataSourceByKey(url: String) {
        runningDataSource.remove(url)
    }
 
    private fun createAndOpenDataSource(url: String) {
        val factory = CustomDataSource.Factory()
        //构造预加载dataSource
        val dataSource = factory.createDataSource()
        runningDataSource.put(url, dataSource)
        try {
            //构建请求参数
            val dataSpec = buildDataSpec(url)
            //预打开数据链接
            val bytesToRead = dataSource.open(dataSpec)
        } catch (e: Exception) {
            runningDataSource.remove(url)
        }
    }
 
    private fun buildDataSpec(url: String, position: Long = 0L): DataSpec {
        val uri = Uri.parse(url)
        return DataSpec.Builder()
            .setUri(uri)
            .setPosition(position)
            //.setKey(customCacheKey)
            .setFlags(
                DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN or DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION
            )
            .setHttpRequestHeaders(createIcyMetadataHeaders())
            .build()
    }
}
### Android平台网易云直播开播教程 #### 准备工作 要在Android平台上使用网易云进行直播开播,开发者需先完成一系列准备工作。这包括但不限于注册并登录网易云信控制台创建应用获取AppKey等必要凭证[^4]。 #### 集成SDK 集成网易云信的SDK到项目中是必要的一步。通常情况下,可以通过Gradle依赖方式快速引入所需库文件。确保按照官方文档指引正确配置build.gradle文件以包含最新的SDK版本信息。 ```gradle dependencies { implementation 'com.netease.nimlib:nim-sdk:[最新版号]' } ``` #### 初始化NIM SDK 在应用程序启动时初始化NIM SDK非常重要。此过程涉及设置核心参数如App Key,并调用相应API方法完成初始化操作。 ```java // 在Application类中的onCreate()方法里执行如下代码 NIMClient.init(context, new NimOptions(appKey)); ``` #### 创建直播间 对于想要发起直播的用户而言,在客户端侧需要构建一个表示“房间”的对象实例并向服务器请求创建该虚拟空间。此时应指定诸如房间名称、类型(单主播或多主播)等相关属性以便后续管理与参与[^1]。 ```java Room room = new Room(); room.setName("测试直播间"); room.setType(RoomType.LIVE); // 设置为直播形式 NIMClient.getService(LiveService.class).createLiveRoom(room); ``` #### 推流准备 当成功建立好直播间之后,则可着手处理实际的音视频采集推送事宜了。这里涉及到摄像头权限申请、预览界面搭建以及最终向远端发送编码后的媒体数据流等一系列动作。 ```java if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.CAMERA}, MY_PERMISSIONS_REQUEST_CAMERA); } SurfaceView surfaceView = findViewById(R.id.surface_view); CameraManager.getInstance().startPreview(surfaceView); Publisher publisher = NERtcKit.getInstance().getRtcEngine().createPublisher(); publisher.startPublish(liveUrl); ``` #### 处理观众连接 考虑到不同类型的直播场景下可能存在的交互需求差异较大,因此针对每种情况都应当有对应的解决方案来保障良好的用户体验。例如,在多主播模式中往往还需要额外考虑如何同步各路信号源的时间戳从而减少视觉上的不一致感;而对于普通观看者来说则更关注能否流畅加载播放内容而不受过多干扰。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值