无缝开播思路
无缝开播的思想是
- 利用播放开播之前的冗余时间,提前建立好网络连接,预加载部分数据,
- 播放器开播时,首先校验是否有已经提前打开的数据流,如果有的话直接加载渲染预加载的数据流,否则执行默认开播流程
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()
}
}