本文为您详细介绍如何使用 PlayerKit 的进阶功能。
PlayerKit 会自动记录播放进度。在播放开始前,PlayerKit 会调用 MediaSource.getSyncProgressId()
,并将其作为索引在 ProgressRecorder
中查找续播记录。若找到记录,PlayerKit 会通过 setStartTime
方法将记录的进度设置到播放器中,使播放器从该进度处开始播放。 因此,若需要启用全局续播功能,您仅需为 MediaSource
实例设置 syncProgressId
。
mediaSource.setSyncProgressId(mediaSource.getMediaId()); // 开启全局续播 mediaSource.setSyncProgressId(null); // 关闭全局续播
若两个 VideoView 的播放源的 MediaId 相同,则支持先解除 VideoView1 的播放器绑定,而后将同一播放器绑定至 VideoView2,实现同一个播放器在 VideoView 间切换的效果。这种用法适用于切换全屏、跨场景续播等场景,能够实现切换场景但播放不中断的效果。
注意
在播放器播放过程中,同一时刻仅能与一个 VideoView 的 PlaybackController 绑定。在进行共享操作前,您需先调用 unbindPlayer 解除绑定。从而避免多个 VideoView 同时接收播放器事件,而仅有一个 VideoView 显示画面的情况。
String mediaId = "same vid string"; // 场景一 MediaSource mediaSource1 = MediaSource.createUrlSource(mediaId, url1, cacheKey1); videoView1.bindDataSource(mediaSource1); videoView1.startPlayback(); // 场景一切换至场景二 // 进入场景二之前,先解绑播放器 videoView1.controller().unbindPlayer(); // 场景二:设置相同 mediaId 的播放源,调用 startPlayback 即可使用场景一已解绑的播放器继续播放。 MediaSource mediaSource2 = MediaSource.createUrlSource(mediaId, url2, cacheKey2); videoView2.bindDataSource(mediaSource2); videoView2.startPlayback(); // 场景二切换至场景一 // 回到场景一之前,先解绑播放器 videoView2.controller().unbindPlayer(); // 回到场景一,起播 videoView1.startPlayback();
通过配置首帧和播放中缓存超时参数,并在 PlaybackController 的播放监听器中捕获特定错误码实现精准缓存超时报错监听。示例代码如下:
VolcConfig volcConfig = new VolcConfig(); // 首帧 loading 30 秒未出首帧则报错,取值范围 [5000, Integer.MAX] volcConfig.firstFrameBufferingTimeoutMS = 30000; // 播放中 loading 60 秒未结束则报错. 取值范围 [10000, Integer.MAX] volcConfig.playbackBufferingTimeoutMS = 60000; VolcConfig.set(mediaSource, volcConfig); // 监听播放出错 playbackController.addPlaybackListener(new EventListener() { @Override public void onEvent(Event event) { switch (event.code()) { case PlayerEvent.State.ERROR: StateError e = e.cast(StateError.class); PlayerException exception = e.e; if (exception.code == PlayerException.CODE_BUFFERING_TIME_OUT) { // 触发缓存超时报错 } break; } } });
外挂字幕指与视频文件分离的字幕文件,用户可在播放时按需导入。PlayerKit 支持添加 WebVTT (Web Video Text Tracks) 和 SRT (SubRip Text) 格式的外挂字幕。外挂字幕的优势在于其灵活性,用户可按需选择是否加载字幕以及加载何种语言的字幕,且无需进行视频转码,只需在播放端设置即可显示。
注意
该功能仅高级版支持。请确保您已购买高级版的 License,详见播放器 License。
外挂字幕的实现流程具体如下:
开启字幕:
// 语言 Id 映射表参考:https://www.volcengine.com/docs/4/1186356 public static final int LANGUAGE_ID_CN = 1; // 简体中文 public static final int LANGUAGE_ID_US = 2; // 英语 // 绑定播放配置 VolcConfig volcConfig = new VolcConfig(); VolcConfig.set(mediaSource, volcConfig); // 获取播放配置 VolcConfig volcConfig = VolcConfig.get(mediaSource); // 开启字幕。默认值:false volcConfig.enableSubtitle = true; // 开启字幕策略预加载。默认值:false volcConfig.enableSubtitlePreloadStrategy = true; // vid/videomodel 使用 vid 字幕支持设置字幕偏好语言,减少字幕地址获取量。默认值:null volcConfig.subtitleLanguageIds = Arrays.asList(LANGUAGE_ID_CN, LANGUAGE_ID_US);
构造字幕源:
Vid 字幕:Vid 字幕支持 Vid 播放源。
String subtitleAuthToken = "Your subtitle auth token"; mediaSource.setSubtitleAuthToken(subtitleAuthToken);
DirectURL 字幕:DirectURL 字幕支持 DirectURL 和 Vid 播放源。
// 详见 [JSON 字幕信息说明](https://www.volcengine.com/docs/4/1166817#json-%E5%AD%97%E5%B9%95%E4%BF%A1%E6%81%AF%E8%AF%B4%E6%98%8E) String subtitleJsonModel = ""; List<Subtitle> subtitles = Mapper.subtitleModelString2Subtitles(subtitleJsonModel); mediaSource.setSubtitles(subtitles);
字幕播放:
起播字幕语言选择:
final SubtitleSelector subtitleSelector = new SubtitleSelector() { @NonNull @Override public Subtitle selectSubtitle(@NonNull MediaSource mediaSource, @NonNull List<Subtitle> subtitles) { // 起播 + 预加载,字幕选择全局回调 // 1. 优先用户上次选择的字幕语言 final int languageId = VideoSubtitle.getUserSelectedLanguageId(mediaSource); if (languageId > 0) { for (Subtitle subtitle : subtitles) { if (subtitle.getLanguageId() == languageId) { return subtitle; } } } // 2. 按照偏好语言优先级返回语言 final List<Integer> preferredLanguageIds = VolcConfig.get(mediaSource).subtitleLanguageIds; if (preferredLanguageIds != null && !preferredLanguageIds.isEmpty()) { for (int languageId : preferredLanguageIds) { for (Subtitle subtitle : subtitles) { if (subtitle.getLanguageId() == languageId) { return subtitle; } } } } // 3. 若都未命中,兜底返回第 0 个 return subtitles.get(0); } }; VolcPlayerInit.config(new VolcPlayerInitConfig.Builder() // ... .setSubtitleSelector(subtitleSelector) // ... .build()
播放中切换语言:
// vid 字幕可监听 PlayerEvent.Info.SUBTITLE_LIST_INFO_READY 回调后,获取字幕列表 // 可用于字幕选择框展示 List<Subtitle> subtitles = player.getSubtitles(); // 用户点击选择了某个字幕后,切换字幕 player.selectSubtitle(subtitle); // 获取当前选择的字幕 Subtitle selected = player.getSelectedSubtitle(); // 获取当前展示的字幕 Subtitle current = player.getCurrentSubtitle();
开启或关闭字幕输出:
player.setSubtitleEnabled(true/false); // 开启或关闭字幕输出 boolean isSubtitleEnabled = player.isSubtitleEnabled(); // 是否开启字幕输出
监听字幕回调:
final EventListener listener = new Dispatcher.EventListener() { @Override public void onEvent(Event event) { switch(event.code()) { case PlayerEvent.Info.SUBTITLE_STATE_CHANGED: { // 字幕开启状态变化回调。 // 用于字幕 TextView 的显示与隐藏 boolean isSubtitleEnabled = player.isSubtitleEnabled(); break; } case PlayerEvent.Info.SUBTITLE_LIST_INFO_READY: { // 字幕信息获取成功 // vid 字幕可在该回调后获取字幕列表 List<Subtitle> subtitles = player.getSubtitles(); break; } case PlayerEvent.Info.SUBTITLE_FILE_LOAD_FINISH: { // 字幕文件加载完成 InfoSubtitleFileLoadFinish e = event.cast(InfoSubtitleFileLoadFinish.class); boolean isSuccess = e.success == 1; // 字幕是否下载成功 break; } case PlayerEvent.Info.SUBTITLE_WILL_CHANGE: { // 调用 player.selectSubtitle(targetSubtitle); 后回调 InfoSubtitleWillChange e = event.cast(InfoSubtitleWillChange.class); Subtitle currentSubtitle = e.current; // 当前展示的字幕,若未展示则为 null Subtitle targetSubtitle = e.target; // 正在切换的目标字幕 break; } case PlayerEvent.Info.SUBTITLE_CHANGED: { // 字幕切换成功回调 InfoSubtitleChanged e = event.cast(InfoSubtitleChanged.class); Subtitle currentSubtitle = e.current; // 当前展示的字幕 Subtitle preSubtitle = e.pre; // 切换前的字幕,若首次切换则为 null break; } case PlayerEvent.Info.SUBTITLE_TEXT_UPDATE: { // 字幕文字回调 InfoSubtitleTextUpdate e = event.cast(InfoSubtitleTextUpdate.class); SubtitleText subtitleText = e.subtitleText; String text = subtitleText.text; // 字幕文字内容,使用 TextView 展示即可 break; } case PlayerEvent.Info.SUBTITLE_CACHE_UPDATE: { // 字幕加载缓存命中回调 InfoSubtitleCacheUpdate e = event.cast(InfoSubtitleCacheUpdate.class); long cachedSizeHint = e.cachedBytes; // 缓存命中大小 break; } } } }; playbackController.addPlaybackListener(listener);
字幕预加载:
说明
仅支持 DirectURL 字幕和 DirectURL 播放源组合。
// 开启短视频场景策略(包含预加载 + 预渲染) VolcEngineStrategy.setEnabled(VolcScene.SCENE_SHORT_VIDEO, true); // 关闭短视频场景策略 VolcEngineStrategy.setEnabled(VolcScene.SCENE_SHORT_VIDEO, false); // 下拉刷新 VolcEngineStrategy.setMediaSourcesAsync(() -> new ArrayList(mediaSources)); // 加载更多 VolcEngineStrategy.addMediaSourcesAsync(() -> new ArrayList(mediaSources));
PlayerKit 内置 H.265 硬解机型黑名单并支持硬解优化。
说明
完整流程请见播放 H.265 视频。
注意
该功能仅高级版支持。请确保您已购买高级版的 License,详见播放器 License。
以下是播放 H.265 视频的关键步骤说明:
获取设备是否支持 H.265 硬解:
boolean isSupportH265HardwareDecode = CodecStrategy.Decoder.isSupport(CodecStrategy.Dimension.h265_HARDWARE);
获取播放源编码类型和解码器类型:
private final Dispatcher.EventListener mPlaybackListener = new Dispatcher.EventListener() { @Override public void onEvent(Event event) { switch (event.code()) { case PlayerEvent.Info.VIDEO_RENDERING_START: { /** * 获取播放源编码类型: * {@link #CODEC_ID_UNKNOWN}, * {@link #CODEC_ID_H264}, * {@link #CODEC_ID_H265}, * {@link #CODEC_ID_H266} */ @CodecId int codecId = player.getVideoCodecId(); /** * 获取播放器解码器类型: * {@link #DECODER_TYPE_UNKNOWN}, * {@link #DECODER_TYPE_SOFTWARE}, * {@link #DECODER_TYPE_HARDWARE} */ @DecoderType int decoderType = player.getVideoDecoderType(); // ... break; } } } }; playbackController.addPlaybackListener(mPlaybackListener);
构造播放源:
DirectUrl 播放源:
String mediaId = "your video id"; String url; // 根据机型是否支持 H.265 硬解选择 H.265 或 H.264 地址进行播放 if (CodecStrategy.Decoder.isSupport(CodecStrategy.Dimension.h265_HARDWARE)) { url = "https://example.volcengine.com/h265.mp4"; } else { url = "https://example.volcengine.com/h264.mp4"; } String cacheKey = MD5.md5(new URL(url).getPath()); // 选传,一般使用 url 去掉时间戳的 path 部分作为缓存 key MediaSource mediaSource = MediaSource.createUrlSource(mediaId, url, cacheKey)
Vid 播放源:
// 1. 构造 Vid 播放源 String mediaId = "your video id"; String playAuthToken = "your video id's playAuthToken"; MediaSource mediaSource = MediaSource.createIdSource(mediaId, playAuthToken); // 2. 根据机型是否支持 H.265 硬解选择 H.265 或 H.264 地址进行播放 VolcConfig volcConfig = new VolcConfig(); if (CodecStrategy.Decoder.isSupport(CodecStrategy.Dimension.h265_HARDWARE)) { volcConfig.sourceEncodeType = Track.ENCODER_TYPE_H265; } else { volcConfig.sourceEncodeType = Track.ENCODER_TYPE_H264; } VolcConfig.set(mediaSource, volcConfig);
vod-scenekit 模块提供短视频场景控件 (ShortVideoSceneView) 和中视频场景控件 (FeedVideoSceneView),已内置相应场景的策略和最佳实践配置。您可参考 vod-scenekit 中的代码实现预加载和预渲染策略。其中:
注意
该功能仅高级版支持。请确保您已购买高级版的 License,详见播放器 License。
以下是使用预加载和预渲染策略的关键步骤说明:
开启和关闭策略:
说明
如果您的 App 存在多个短视频页签切换或者多个短视频页面嵌套的场景时,需要注意关闭策略的时机。更多信息,请见策略注意事项。
int scene = VolcScene.SCENE_SHORT_VIDEO; // 短视频场景 // int scene = VolcScene.SCENE_FEED_VIDEO; // 中视频场景 // 开启策略 VolcEngineStrategy.setEnabled(scene, true); // 关闭策略 VolcEngineStrategy.setEnabled(scene, false);
设置播放列表,执行预加载和预渲染。
List<MediaSource> mediaSources = ...; // 下拉刷新、切换场景、播放列表有改变时可以使用 setMediaSources 更新播放列表 VolcEngineStrategy.setMediaSourcesAsync(() -> new ArrayList(mediaSources)); // 加载更多场景,使用 addMediaSources 往后追加播放列表 VolcEngineStrategy.addMediaSourcesAsync(() -> new ArrayList(mediaSources));
使用预渲染的视频首帧替代视频封面:
public static boolean renderFrame(VideoView videoView) { if (videoView == null) return false; int[] frameInfo = new int[2]; VolcEngineStrategy.renderFrame(videoView.getDataSource(), videoView.getSurface(), frameInfo); int videoWidth = frameInfo[0]; int videoHeight = frameInfo[1]; if (videoWidth > 0 && videoHeight > 0) { videoView.setDisplayAspectRatio(DisplayModeHelper.calDisplayAspectRatio(videoWidth, videoHeight, 0)); return true; } return false; }
PlayerKit 支持自适应码率 (ABR) 播放,可根据网络带宽自动选择起播清晰度,提升起播速度。
注意
ttsdkPlayerExtensions: "abr"
。如果您还需实现超分降档,则配置 ttsdkPlayerExtensions: "super_resolution,abr"
。以下是实现起播选档的关键步骤说明:
初始化:
final VolcConfigUpdater configUpdater = new VolcConfigUpdater() { @Override public void updateVolcConfig(MediaSource mediaSource) { VolcConfig config = VolcConfig.get(mediaSource); if (config.qualityConfig == null) return; if (!config.qualityConfig.enableStartupABR) return; final int qualityRes = VideoQuality.getUserSelectedQualityRes(mediaSource); if (qualityRes <= 0) { config.qualityConfig.userSelectedQuality = null; } else { config.qualityConfig.userSelectedQuality = VolcQuality.quality(qualityRes); } } }; VolcPlayerInit.config(new VolcPlayerInitConfig.Builder() // ... .setConfigUpdater(configUpdater) // ... .build()); );
以短视频场景为例,配置播放器起播选档策略:
/** * 短视频场景起播选档配置 */ public static VolcQualityConfig createShortVideoVolcConfig() { final VolcQualityConfig config = new VolcQualityConfig(); // 开启 ABR 播放,根据网络带宽自动选择起播清晰度,提升起播速度 config.enableStartupABR = true; // 开启超分降档 config.enableSupperResolutionDowngrade = false; // 无测速信息时,默认清晰度,这里设置 720P(默认值 720P) config.defaultQuality = VolcQuality.QUALITY_720P; // WIFI 清晰度上限,这里设置 720P (默认值 720P) config.wifiMaxQuality = VolcQuality.QUALITY_720P; // 移动网络清晰度上限,这里设置 720P(默认值 360P) config.mobileMaxQuality = VolcQuality.QUALITY_720P; final VolcQualityConfig.VolcDisplaySizeConfig displaySizeConfig = new VolcQualityConfig.VolcDisplaySizeConfig(); config.displaySizeConfig = displaySizeConfig; final int screenWidth = UIUtils.getScreenWidth(VolcPlayerInit.getContext()); final int screenHeight = UIUtils.getScreenHeight(VolcPlayerInit.getContext()); // 屏幕宽 displaySizeConfig.screenWidth = screenWidth; // 屏幕高 displaySizeConfig.screenHeight = screenHeight; // 播放 View 宽 displaySizeConfig.displayWidth = (int) (screenHeight / 16f * 9); // 播放 View 高 displaySizeConfig.displayHeight = screenHeight; return config; }
您从视频点播服务获取的视频播放地址默认已启用时间戳防盗链功能。在视频播放时,视频地址可能因未及时使用、用户执行 Seek 播放或循环播放等操作而过期,从而导致视频无法继续播放。PlayerKit 支持配置自动刷新视频播放地址,即使视频播放地址过期,也能自动重新获取有效的播放地址,以确保用户可继续观看视频。 以下是实现播放源过期 403 自刷新的关键步骤说明:
初始化:
// 1. 初始化,设置刷新 CDN 地址的 Fetcher Factory // 参考 https://github.com/volcengine/VEVodDemo-android/blob/main/vod-demo/src/main/java/com/bytedance/volc/voddemo/VodSDK.java#L132 VolcPlayerInit.config(new VolcPlayerInitConfig.Builder() // ... .setUrlRefreshFetcherFactory(new AppUrlRefreshFetcher.Factory()) // ... .build()); // 2. 实现 VolcUrlRefreshFetcher,在 fetch 方法中请求 appServer 刷新 URL。 // - 成功回调 onSuccess,返回 「刷新后的 url」和「过期时间」。 // - 失败回调 onError,返回「错误码」和「错误信息」。 // 参考:https://github.com/volcengine/VEVodDemo-android/blob/main/vod-demo/src/main/java/com/bytedance/volc/voddemo/video/AppUrlRefreshFetcher.java public class AppUrlRefreshFetcher implements VolcUrlRefreshFetcher { private static final int ERROR_CODE_HTTP_ERROR = -1; private static final int ERROR_CODE_RESULT_NULL = -2; public static class Factory implements VolcUrlRefreshFetcher.Factory { @Override public AppUrlRefreshFetcher create() { return new AppUrlRefreshFetcher(); } } private Call<GetRefreshUrlResponse> mCall; @Override public void fetch(VolcUrlRequest request, Callback callback) { L.v(this, "fetch", request.mediaId, request.cacheKey, request.url); final Call<GetRefreshUrlResponse> call = ApiManager.api2().getRefreshUrl(new GetRefreshUrlRequest(request.url)); call.enqueue(new retrofit2.Callback<GetRefreshUrlResponse>() { @Override public void onResponse(@NonNull Call<GetRefreshUrlResponse> call, @NonNull Response<GetRefreshUrlResponse> response) { if (response.isSuccessful()) { final GetRefreshUrlResponse ret = response.body(); if (ret == null || ret.result == null) { notifyError(callback, request, ERROR_CODE_RESULT_NULL, "result is empty!"); } else { notifySuccess(callback, request, new VolcUrlResult(ret.result.url, ret.result.expireInS * 1000L)); } } else { notifyError(callback, request, ERROR_CODE_HTTP_ERROR, "httpCode:" + response.code() + " " + response); } } @Override public void onFailure(@NonNull Call<GetRefreshUrlResponse> call, @NonNull Throwable t) { notifyError(callback, request, ERROR_CODE_HTTP_ERROR, String.valueOf(t)); } }); mCall = call; } @Override public void cancel() { if (mCall != null) { mCall.cancel(); } } private void notifySuccess(Callback callback, VolcUrlRequest request, VolcUrlResult urlInfo) { L.v(this, "notifySuccess", request.mediaId, request.cacheKey, request.url, urlInfo.url, urlInfo.expireTimeInMS); callback.onSuccess(urlInfo); } private void notifyError(Callback callback, VolcUrlRequest request, int errorCode, String errorMsg) { L.v(this, "notifyError", request.mediaId, request.cacheKey, request.url, errorCode, errorMsg); callback.onError(errorCode, errorMsg); } }
创建播放源并开启播放源自刷新:
// 1. 创建播放源 MediaSource mediaSource = MediaSource.createIdSource(videoId, playAuthToken) MediaSource mediaSource = MediaSource.createUrlSource(videoId, videoUrl, videoCacheKey); // 2. 开启播放源自刷新 VolcConfig volcConfig = new VolcConfig(); volcConfig.enable403SourceRefreshStrategy = true; VolcConfig.set(mediaSource, volcConfig);