本文为您详细介绍如何使用 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 = event.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);
画中画(Picture-in-Picture, PiP)功能允许用户在屏幕一角的小窗口中观看视频,同时可以在主屏幕上继续与其他应用或内容进行交互。这对于需要多任务处理的场景(如一边观看视频教程一边操作)非常有用。
注意
请确保 Android 版本为 Android 6.0 (API level 23) 或更高版本。Android 6.0 以上提供了标准的悬浮窗权限申请 API,低于此版本则需要兼容各厂商的私有 API。
实现画中画功能的核心流程如下:
PipWindowView,封装窗口的创建、显示和隐藏、手势拖动、边缘吸附等通用逻辑。player,仅切换视频流渲染的 Surface ,从而实现无缝播放体验。本文仅为您介绍实现画中画功能的核心步骤。如需了解实现细节,可点击以下表格中的链接前往 GitHub 查看源码。
模块 | 关键类 | 说明 |
|---|---|---|
vod-demo | 悬浮窗权限申请。旨在简化悬浮窗权限申请流程,内部使用的权限申请 API 要求系统版本需大于等于 Android 6.0,若不满足此条件则返回申请失败。 | |
悬浮窗权限申请代理 Activity。用于监听 | ||
悬浮窗控制器。控制在主播放界面和画中画窗口之间切换的逻辑。 | ||
vod-scenekit | 画中画视频场景。实现播放列表播放。 | |
通用悬浮窗控件。 |
在应用启动或进入画中画功能前,需要检查并申请悬浮窗权限。
查询是否授权:将 Demo 中提供的 PipVideoPermission 类拷贝至您自己的项目中,再参考以下示例代码检查应用是否已被授予悬浮窗权限。
PipVideoPermission pipVideoPermission = new PipVideoPermission(context); // 查询用户是否授权 boolean isPermissionGranted = pipVideoPermission.isPermissionGranted();
动态申请权限:如果用户未授权,则需要引导用户到系统设置页面手动开启。
将 Demo 中提供的 PipVideoPermissionActivity 拷贝至您自己的项目中,然后在 AndroidManifest.xml 中声明权限和用于处理权限回调的 Activity:
说明
您需要将 com.bytedance.volc.voddemo.ui.video.scene.pipvideo.permission.PipVideoPermissionActivity 替换为您自己项目中的实际路径。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <Application> <activity android:name="com.bytedance.volc.voddemo.ui.video.scene.pipvideo.permission.PipVideoPermissionActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard" android:theme="@style/VEVodAppTheme.PermissionBridge" /> </Application>
在需要触发画中画时调用权限申请逻辑:
public void requestMainToPip() { if (pipVideoPermission.isPermissionGranted()) { // 已授权,处理主播放界面切换小窗逻辑 } else { // 未授权,动态申请权限,用户需要到系统设置中开启权限 pipVideoPermission.requestPermission(new PipVideoPermission.Callback() { @Override public void onResult(boolean isGranted) { if (isGranted) { // 已授权,处理主播放界面切换小窗逻辑 } else { // 未授权 Toast.makeText(context, "未开启权限,小窗切换失败...", Toast.LENGTH_SHORT).show(); } } @Override public void onRationale(Context context, PipVideoPermission.UserAction userAction) { // 展示权限申请说明 new AlertDialog.Builder(context) .setMessage("尚未开启系统悬浮窗,请去设置中开启 [显示悬浮窗] 权限") .setPositiveButton("去开启", (dialog, which) -> { // 用户同意开启 userAction.granted(); }) .setNegativeButton("取消", (dialog, which) -> { // 用户未同意 userAction.denied(); }) .setCancelable(false) .show(); } }); } }
将 Demo 中提供的 PipWindowView 类拷贝至您自己的项目中,再参考以下示例代码使用 PipWindowView 作为画中画的容器。PipWindowView 内部实现了悬浮窗的显示隐藏、手势拖动、贴边动画等功能,不包含视频相关逻辑,适合任何悬浮窗场景使用。
创建悬浮窗:
// 创建悬浮窗控件 PipWindowView PipWindowView pipWindowView = new PipWindowView(context);
配置悬浮窗:
// 设置悬浮窗大小,单位:px pipWindowView.setWindowInitWidth(width); pipWindowView.setWindowInitHeight(height); // 设置悬浮窗距离屏幕边缘的 margin,单位:px pipWindowView.setWindowMargin(margin); // 设置悬浮窗显示位置坐标,单位:px pipWindowView.setWindowInitX(x); pipWindowView.setWindowInitY(y); // 设置是否在显示时使用设置的大小、坐标信息 pipWindowView.setInitShow(true); // 设置悬浮窗圆角,单位:px pipWindowView.setRadius(radius); // 设置悬浮窗背景颜色 pipWindowView.setCardBackgroundColor(Color.BLACK); // 设置悬浮窗 Z 轴高度,以及阴影效果,单位:px pipWindowView.setCardElevation(elevation);
显示和隐藏悬浮窗:
// 显示 pipWindowView.show(); // 隐藏 pipWindowView.dismiss(); // 是否显示 boolean isShowing = pipWindowView.isShowing();
切换的核心是复用播放器 player 实例,只改变其渲染的 Surface。以下代码示例假设您已在您的 Activity 或 Fragment 中初始化了播放器 player 实例及用于主播放界面的 mainVideoView。
进入画中画模式:当用户触发进入画中画模式时,参考 Demo 中 PipVideoController 的 mainToPip 方法实现从主播放界面切换至悬浮小窗:
// 1. 主播放界面 VideoView 解绑播放器 PlaybackController mainController = mainVideoView.controller(); mainController.unbindPlayer(); // 2. 创建小窗 VideoView,并显示小窗 VideoView pipVideoView = createPipVideoView(context); pipWindowView.add(pipVideoView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); pipWindowView.show(); // 3. 小窗 VideoView 绑定主播放界面的播放源 MediaSource mediaSource = mainVideoView.getDataSource(); pipVideoView.bindDataSource(mediaSource); // 4. 小窗 VideoView 开启播放流程,内部会自动复用主播放界面的播放器继续播放。 pipVideoView.startPlayback();
退出画中画模式:当用户从画中画窗口返回应用时,参考 Demo 中 PipVideoController 的 pipToMain 方法从悬浮小窗切换至主播放界面:
// 1. 小窗 VideoView 解绑播放器 PlaybackController pipController = pipVideoView.controller(); pipController.unbindPlayer(); // 2. 隐藏小窗 pipWindowView.dismiss(); // 3. 主播放界面的 VideoView 开启播放流程,内部会自动复用小窗的播放器继续播放 mainVideoView.startPlayback();