P2P配置
用建造者模式实例化 P2pConfig,以下的参数是默认值:
val config = P2pConfig.Builder()
.logEnabled(false) // 是否打印日志
.logLevel(LogLevel.WARN) // 打印日志的级别
.trackerZone(TrackerZone.Europe) // tracker服务器地址所在国家的枚举,分为China、Europe、HongKong、USA
.downloadTimeout(30_000, TimeUnit.MILLISECONDS) // HTTP下载ts文件超时时间
.localPortHls(0) // HLS本地代理服务器的端口号(默认随机端口)
.localPortDash(0) // DASH本地代理服务器的端口号(默认随机端口)
.diskCacheLimit(2000*1024*1024) // 点播模式下P2P在磁盘缓存的最大数据量(设为0可以禁用磁盘缓存)
.memoryCacheCountLimit(15) // P2P在内存缓存的最大数据量,用ts文件个数表示
.p2pEnabled(true) // 开启或关闭p2p engine
.withTag(null) // 用户自定义的标签,可以在控制台查看分布图
.webRTCConfig(null) // 通过webRTCConfig来修改WebRTC默认配置
.maxPeerConnections(25) // 最大连接节点数量
.useHttpRange(true) // 在可能的情况下使用Http Range请求来补足p2p下载超时的剩余部分数据
.useStrictHlsSegmentId(false) // 使用基于url的SegmentId,替代默认基于序列号的
.httpHeadersForHls(null) // 设置请求ts和m3u8时的HTTP请求头
.httpHeadersForDash(null) // 设置请求Dash文件时的HTTP请求头
.isSetTopBox(false) // 如果运行于机顶盒请设置成true
.sharePlaylist(false) // 是否允许m3u8文件的P2P传输
.prefetchOnly(false) // HLS模式下只采用预加载的方式进行P2P下载
.logPersistent(false) // 是否将日志持久化到外部存储(Environment.getExternalStorageDirectory()路径下的logger文件夹)
.geoIpPreflight(true) // 向在线IP数据库请求ASN等信息,从而获得更准确的调度
.insertTimeOffsetTag(null) // 仅在直播模式生效,在m3u8文件中插入 "#EXT-X-START:TIME-OFFSET=[timeOffset]",强制播放器从某个位置开始加载,其中 [timeOffset] 是在播放列表的偏移量,如果为负则从播放列表结尾往前偏移(单位:秒)
.p2pProtocolVersion(P2pProtocolVersion.V8) // P2P 协议的版本号,与其他平台互通的前提是 P2P 协议版本号相同
.dashMediaFiles(
arrayListOf("mp4", "fmp4", "webm", "m4s", "m4v")) // 支持的DASH媒体文件后缀
.build()
P2pEngine
实例化P2pEngine,获得一个全局单例:
P2pEngine.init(context, token, config)
P2pEngine.init(context, token, config);
参数说明:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
context | Context | 是 | 建议使用Application 的 Context 对象。 |
token | String | 是 | CDNBye分配的token。 |
config | P2pConfig | 否 | 自定义配置。 |
切换源
当播放器切换到新的播放地址时,只需要将新的播放地址传给 P2pEngine,从而获取新的本地播放地址:
val parsedUrl = P2pEngine.instance.parseStreamUrl(url)
String parsedUrl = P2pEngine.getInstance().parseStreamUrl(url);
P2pEngine API
P2pEngine.version
当前插件的版本号。
P2pEngine.instance
获取 P2pEngine 的单例。
engine.parseStreamUrl(url: String)
将原始播放地址(m3u8/mpd)转换成本地代理服务器的地址。
engine.parseStreamUrl(url: String, videoId: String)
除了原始播放地址(m3u8/mpd),同时传入videoId用以构造channelId。
engine.parseStreamUrl(url: String, videoId: String, mimeType: MimeType)
除了原始播放地址(m3u8/mpd)以及videoId,同时传入mimeType(目前支持MimeType.APPLICATION_M3U8和MimeType.APPLICATION_MPD),用于无".m3u8"或".mpd"后缀的url。
engine.isConnected
是否已与CDNBye后台建立连接。
engine.stopP2p()
立即停止P2P加速并释放资源,一般只需要在退出APP的时候调用即可。SDK采用"懒释放"的策略,只有在重启p2p的时候才释放资源。对于性能较差的设备起播耗时可能比较明显,建议在视频播放之前提前调用 engine.stopP2p() 。
engine.restartP2p()
重启P2P加速服务,一般不需要调用。
engine.peerId
获取对等连接的id。
engine.setHttpHeadersForHls(headers: Map<String, String>?)
动态设置请求ts和m3u8时的HTTP请求头。
engine.setHttpHeadersForDash(headers: Map<String, String>?)
动态设置请求Dash文件时的HTTP请求头。
engine.notifyPlaybackStalled()
当有卡顿发生时通知SDK。
engine.disableP2p()
运行时动态关闭P2P,在播放下一个媒体文件时才生效。
engine.enableP2p()
运行时动态开启P2P,在播放下一个媒体文件时才生效。
engine.shutdown()
停止P2P并关闭代理服务器。
P2P统计
通过 P2pStatisticsListener 来监听P2P下载信息:
engine.addP2pStatisticsListener(object : P2pStatisticsListener {
override fun onHttpDownloaded(value: Int) {
}
override fun onP2pDownloaded(value: Int, speed: Int) {
}
override fun onP2pUploaded(value: Int, speed: Int) {
}
override fun onPeers(peers: List<String>) {
}
override fun onServerConnected(connected: Boolean) {
}
})
engine.addP2pStatisticsListener(new P2pStatisticsListener() {
@Override
public void onHttpDownloaded(int value) {
}
@Override
public void onP2pDownloaded(int value, int speed) {
}
@Override
public void onP2pUploaded(int value, int speed) {
}
@Override
public void onPeers(@NonNull List<String> peers) {
}
@Override
public void onServerConnected(boolean connected) {
}
});
WARNING
下载和上传数据量的单位是KB,下载速度的单位是KB/s。
高级用法
回调播放器信息
在直播模式下,为了增强P2P效果并提高播放流畅度,建议通过 setPlayerInteractor ,将从当前播放时间到缓冲前沿的时间间隔回调给p2p engine。
P2pEngine.instance?.setPlayerInteractor(object : PlayerInteractor() {
override fun onBufferedDuration(): Long {
return if (player != null) {
// Exoplayer 单位:毫秒
player!!.bufferedPosition - player!!.currentPosition
} else {
-1
}
}
})
P2pEngine.getInstance().setPlayerInteractor(new PlayerInteractor() {
public long onBufferedDuration() {
// Exoplayer in milliseconds
if (play != null) {
return player.getBufferedPosition() - player.getCurrentPosition();
}
return -1;
}
});
在点播模式下,一个视频的时长可能比较大,如果能在节点匹配的时候优先匹配播放时间接近的节点则有助于P2P效果的提升,要实现这个功能需要在SDK层面获取播放器的当前播放时间:
P2pEngine.instance?.setPlayerInteractor(object : PlayerInteractor() {
override fun onCurrentPosition(): Long {
// Exoplayer 单位:毫秒
return player?.currentPosition ?: -1
}
})
P2pEngine.getInstance().setPlayerInteractor(new PlayerInteractor() {
public long onCurrentPosition() {
// Exoplayer 单位:毫秒
if (play != null) {
return player.getCurrentPosition();
}
return -1;
}
});
解决动态url路径问题
某些CDN提供商提供的url是动态生成的,不同节点的地址不一样,例如example.com/clientId1/streamId.m3u8和example.com/clientId2/streamId.m3u8, 而本插件默认使用url(去掉查询参数)作为channelId。这时候就要构造一个共同的chanelId,使实际观看同一直播/视频(下载同一文件)的节点处在相同频道中。构造channelId方法如下:
val videoId = extractVideoIdFromUrl(urlString) // extractVideoIdFromUrl 需要自己定义,可以抽取url中的视频ID作为结果返回
val parsedUrl = P2pEngine.instance?.parseStreamUrl(urlString, videoId)
String videoId = extractVideoIdFromUrl(urlString); // extractVideoIdFromUrl 需要自己定义,可以抽取url中的视频ID作为结果返回
String parsedUrl = P2pEngine.getInstance().parseStreamUrl(urlString, videoId);
WARNING
如果要与其他平台互通,则必须确保两者拥有相同的 token 和 channelId 。
严格切片ID模式
使用基于url的SegmentId,替代默认基于序列号的:
P2pEngine.instance?.setHlsSegmentIdGenerator(StrictHlsSegmentIdGenerator())
P2pEngine.getInstance().setHlsSegmentIdGenerator(new StrictHlsSegmentIdGenerator());
设置HTTP请求头
出于防盗链或者统计的需求,有些HTTP请求需要加上 User-Agent 等头信息,可以通过 setHttpHeaders 进行设置:
val headers = mapOf("User-Agent" to "XXX")
P2pEngine.instance?.setHttpHeadersForHls(headers)
P2pEngine.instance?.setHttpHeadersForDash(headers)
Map headers = new HashMap();
headers.put("User-Agent", "XXX");
engine.setHttpHeadersForHls(headers);
engine.setHttpHeadersForDash(headers);
设置播放时间偏移量
通过在m3u8设置一个特殊的tag,可以强制播放器从列表开始位置加载,从而提升P2P效果,但同时会增加延迟,需要权衡考虑。
val config = P2pConfig.Builder()
.insertTimeOffsetTag(0.0)
.build()
P2pConfig config = new P2pConfig.Builder()
.insertTimeOffsetTag(0.0)
.build();
播放器卡顿统计
SwarmCloud 控制台可以监测客户端的平均卡顿率,只需要在播放器卡顿发生时上报给SDK即可,以 Exoplayer 为例:
player?.addListener(object : Player.Listener {
var isDetecting = false
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState == 2) { // STATE_BUFFERING
if (isDetecting) return
isDetecting = true
Timer().schedule(object : TimerTask() {
override fun run() {
runOnUiThread {
isDetecting = false
if (!player!!.isPlaying) {
P2pEngine.instance!!.notifyPlaybackStalled()
}
}
}
}, 7000)
}
}
})
player.addListener(new Player.Listener() {
Boolean isDetecting = false;
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == 2) { // STATE_BUFFERING
if (isDetecting) return;
isDetecting = true;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
isDetecting = false;
if (!player.isPlaying()) {
P2pEngine.getInstance().notifyPlaybackStalled();
}
}
});
}
}, 8000);
}
}
});
拦截m3u8或mpd文件
由于 SDK 需要解析m3u8/mpd的内容,如果您用了加密的m3u8/mpd,则需要使用拦截器拦截并返回标准的m3u8/mpd文件:
P2pEngine.instance?.setHlsInterceptor(object : HlsInterceptor() {
override fun interceptPlaylist(text: ByteArray, url: String): ByteArray {
return handlePlaylist(text, url);
}
})
P2pEngine.getInstance().setHlsInterceptor(new HlsInterceptor() {
@Override
public byte[] interceptPlaylist(byte[] text, String url) {
return handlePlaylist(text, url);
}
});
支持无后缀的媒体文件
有些特殊的文件不是以 ".ts" 结尾,也没有任何其它后缀,此时可以通过hook函数判断其是否是媒体文件:
P2pEngine.instance?.setHlsInterceptor(object : HlsInterceptor() {
override fun isMediaSegment(url: String): Boolean {
return true
}
})
P2pEngine.getInstance().setHlsInterceptor(new HlsInterceptor() {
@Override
public boolean isMediaSegment(@NonNull String url) {
return true;
}
});
传入自定义 OkHttpClient
val httpClient = OkHttpClient.Builder()
.addInterceptor(YourInterceptor())
.build()
val config = P2pConfig.Builder().okHttpClient(httpClient).build()
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(new YourInterceptor())
.build();
P2pConfig config = new P2pConfig.Builder()
.okHttpClient(httpClient)
.build();
屏蔽某些特殊的切片文件
某些情况下我们不想让某些切片文件参与P2P,比如SSAI(Server Side Ad Insertion)产生的特定于用户的切片,这个时候可以利用 segmentBypass 这个函数来进行过滤:
P2pEngine.instance?.setHlsInterceptor(object : HlsInterceptor() {
override fun shouldBypassSegment(url: String): Boolean {
return isSSAISegment(url)
}
})
P2pEngine.getInstance().setHlsInterceptor(new HlsInterceptor() {
@Override
public boolean shouldBypassSegment(@NonNull String url) {
return isSSAISegment(url);
}
});
监听SDK异常信息
由于网络、服务器和算法bug等原因,SDK可能会出现异常,可以通过 registerExceptionListener 方法来监听异常:
P2pEngine.instance?.registerExceptionListener(object : EngineExceptionListener {
override fun onTrackerException(e: EngineException) {
// Tracker Exception
}
override fun onSignalException(e: EngineException) {
// Signal Server Exception
}
override fun onSchedulerException(e: EngineException) {
// Scheduler Exception
}
override fun onOtherException(e: EngineException) {
// Other Exception
}
})
P2pEngine.getInstance().registerExceptionListener(new EngineExceptionListener() {
@Override
public void onTrackerException(EngineException e) {
// Tracker Exception
}
@Override
public void onSignalException(EngineException e) {
// Signal Server Exception
}
@Override
public void onSchedulerException(EngineException e) {
// Scheduler Exception
}
@Override
public void onOtherException(EngineException e) {
// Other Exception
}
});