外挂字幕指与视频文件分离的字幕文件,用户可在播放时按需导入。播放器 SDK 支持添加 WebVTT (Web Video Text Tracks) 和 SRT (SubRip Text) 格式的外挂字幕。外挂字幕的优势在于其灵活性,用户可按需选择是否加载字幕以及加载何种语言的字幕,且无需进行视频转码,只需在播放端设置即可显示。本文介绍如何使用播放器 SDK 添加外挂字幕。
注意
自 1.36.1 版本起,外挂字幕功能由增值服务调整至高级版和企业版。若您集成 1.36.1 之前的版本且想要使用此功能,请升级至最新版本。
出于拓展性的考量,播放器 SDK 只返回字幕文本信息,具体的字幕展示需要您结合 UI 控件自行实现。实现外挂字幕功能的核心流程如下:
play 之前,完成以下所有配置:
setIntOption 方法开启外挂字幕功能及相关优化项。setSubInfoCallBack 注册监听器,以接收字幕内容。onSubInfoCallback 回调中获取 SDK 按时间顺序推送的字幕文本,并将其渲染到您的 UI 控件(如 TextView)上。setIntOption 控制字幕的显示与隐藏。setIntOption 并传入目标语言的 sub_id 实现切换。参考以下示例代码开启外挂字幕功能:
// 外挂字幕功能总开关,play 前调用 // 0: 关闭外挂字幕功能;1: 开启外挂字幕功能 mVideoEngine.setIntOption(PLAYER_OPTION_ENABLE_OPEN_SUB_THREAD, 1); // 外挂字幕加载优化开关 // 0: 关闭外挂字幕加载优化;1: 开启外挂字幕加载优化 mVideoEngine.setIntOption(PLAYER_OPTION_ENABLE_OPT_SUB_LOAD_TIME, 1); // 使用数据加载模块 MDL 加载外挂字幕,提升加载成功率 // 0: 不使用 MDL 加载外挂字幕;1: 使用 MDL 加载外挂字幕 mVideoEngine.setIntOption(PLAYER_OPTION_SUB_ENABLE_MDL, 1);
播放器 SDK 支持以下两种方式设置字幕源。您需根据实际情况选择。
使用 DirectURL 方式设置字幕源,您需要实现 SubDesInfoModelProvider 协议构建外挂字幕源,参考 JSON 字幕信息示例。
说明
若您需要将已经下载到本地设备的离线字幕文件作为字幕源,将 url 设为本地文件路径即可,形如 file:///path/to/your/subtitle.srt。
示例代码如下:
final String vid = "YOUR_VIDEO_ID"; // 播放源与 vid 必须一一对应 final String url = "http://www.example.com/h264.mp4"; final String cacheKey = TTVideoEngine.computeMD5(url); // 1. 构建字幕信息 SubDesInfoModel subtitleModel = null; try { JSONObject subtitleJson = new JSONObject(); JSONArray list = new JSONArray(); JSONObject subtitle1 = new JSONObject(); subtitle1.put("language", "cmn-Hans-CN"); subtitle1.put("language_id", 1); subtitle1.put("url", "YOUR_CHINESE_SUBTITLE_URL"); subtitle1.put("format", "webvtt"); subtitle1.put("sub_id", 0); list.put(subtitle1); JSONObject subtitle2 = new JSONObject(); subtitle2.put("language", "eng-US"); subtitle2.put("language_id", 2); subtitle2.put("url", "YOUR_ENGLISH_SUBTITLE_URL"); subtitle2.put("format", "webvtt"); subtitle2.put("sub_id", 1); list.put(subtitle2); subtitleJson.put("list", list); subtitleModel = new SubDesInfoModel(subtitleJson); } catch (JSONException e) { e.printStackTrace(); } // 2. 设置播放源 DirectUrlSource directUrlSource = new DirectUrlSource.Builder() .setVid(vid) .addItem(new DirectUrlSource.UrlItem.Builder() .setUrl(url) .setCacheKey(cacheKey) .build()) .build(); mVideoEngine.setStrategySource(directUrlSource); // 3. 设置字幕源 // 必须在设置播放源之后且在 play 之前设置 subtitleModel if (subtitleModel != null) { mVideoEngine.setSubDesInfoModel(subtitleModel); }
使用 Vid + SubtitleToken 方式设置字幕源的示例代码如下:
说明
您可在应用服务端通过视频点播服务端 SDK 签发字幕鉴权 Token。
String vid = "YOUR_VIDEO_ID"; // AppServer 下发 String playAuthToken = "YOUR_PLAYAUTHTOKEN"; // AppServer 下发 String subAuthToken = "YOUR_SUBAUTHTOKEN"; // AppServer 下发 // 1. 创建 Vid 播放源 VidPlayAuthTokenSource vidSource = new VidPlayAuthTokenSource.Builder() .setVid(vid) .setPlayAuthToken(playAuthToken) .build(); // 2. 设置播放源 mVideoEngine.setStrategySource(vidSource); // 3. 设置字幕鉴权 token mVideoEngine.setSubAuthToken(subAuthToken); // 4. 设置回调,在获取到视频信息后,进一步处理字幕 mVideoEngine.setVideoInfoListener(new VideoInfoListener() { @Override public boolean onFetchedVideoInfo(VideoModel videoModel) { // 获取视频数据成功回调 Log.v("VideoPlay", "onFetchedVideoInfo " + videoModel); if (videoModel == null) return false; if (mVideoEngine == null) return false; // 5. 获取视频包含的所有字幕信息 List<SubInfo> subInfoList = mVideoEngine.supportedSubInfoList(); if (subInfoList == null || subInfoList.isEmpty()) { return false; } // 6. 根据语言ID筛选需要的字幕,并将其字幕ID添加到列表中 // 字幕语言映射表见:https://www.volcengine.com/docs/4/1186356 List<String> subtitleIds = new ArrayList<>(); for (SubInfo info : subInfoList) { // 例如,筛选出中文和英文字幕 int languageId = info.getValueInt(VALUE_SUB_INFO_LANGUAGE_ID); if (languageId == 1) { // 1 代表中文 subtitleIds.add("" + info.getValueInt(VALUE_SUB_INFO_ID)); } else if (languageId == 2) { // 2 代表英文 subtitleIds.add("" + info.getValueInt(VALUE_SUB_INFO_ID)); } } String subIds = trans(subtitleIds); // 7. 将筛选出的字幕 ID 列表设置给播放器,以便后续加载和显示 mVideoEngine.setStringOption(PLAYER_OPTION_SUB_IDS, subIds); return false; } String trans(List<String> ids){ String ans = ""; for(int i = 0; i < ids.size(); i++){ ans += ids.get(i); if(i < ids.size() - 1){ ans += ","; } } return ans; } });
在调用 play 播放前,调用 setSubInfoCallBack 设置字幕回调及内容。示例代码如下:
mVideoEngine.setSubInfoCallBack(new SubInfoSimpleCallBack() { @Override public void onSubPathInfo(String subPathInfoJson, Error error) { // vid + subTitleToken 播放,字幕列表信息回调 } @Override public void onSubInfoCallback(int code, String infoJson) { // 字幕信息回调 JSONObject jsonObject = new JSONObject(infoJson); String subtitle = jsonObject.optString("info"); textView.setText(subtitle); // 设置给字幕 TextView } @Override public void onSubSwitchCompleted(int success, int subId) { // 字幕语言切换回调 } @Override public void onSubLoadFinished(int success) { // 字幕文件下载结果回调 } });
参考以下示例代码在起播时或者播放过程中控制开启或者关闭字幕输出:
// 字幕开关,起播时或者播放过程中控制打开或者关闭字幕 // 1: 输出字幕;0: 停止输出字幕 mVideoEngine.setIntOption(PLAYER_OPTION_ENABLE_OPEN_SUB, 1); // 显示字幕 TextView textView.setVisibility(View.VISIBLE);
参考以下示例代码切换不同语言的字幕:
说明
您需传入字幕语言 ID,详见语言。
// 业务端维护字幕 ID 与语言的对应关系,方便扩展 mVideoEngine.setIntOption(PLAYER_OPTION_SWITCH_SUB_ID, sub_id);
为提升用户观看视频时字幕的加载速度和成功率,可结合视频预加载策略,在播放前提前缓存字幕文件。此功能需在预加载阶段就指定好视频源和字幕源。
说明
在初始化 SDK 前,开启预加载策略 V2 版本,并启用预加载策略。
// 开启预加载策略 V2 版本 StrategyManager.setVersion(StrategyManager.VERSION_2); // 初始化播放器 SDK Env.init(/* 省略初始化代码 */); // 开启并使用预加载策略 TTVideoEngine.enableEngineStrategy(STRATEGY_TYPE_PRELOAD, STRATEGY_SCENE_SMALL_VIDEO);
构建字幕源和播放源。
private void setPreloadDirectURLSubtitleResource(){ // 1. 创建字幕源 Model SubDesInfoModel subtitleModel = null; // 建议使用 JSONObject 构建 JSON 字符串,避免手动拼接导致格式错误 try { JSONObject subtitleJson = new JSONObject(); JSONArray list = new JSONArray(); JSONObject subtitle1 = new JSONObject(); subtitle1.put("language", "cmn-Hans-CN"); subtitle1.put("language_id", 1); subtitle1.put("url", "YOUR_CHINESE_SUBTITLE_URL"); subtitle1.put("format", "webvtt"); subtitle1.put("sub_id", 0); list.put(subtitle1); JSONObject subtitle2 = new JSONObject(); subtitle2.put("language", "eng-US"); subtitle2.put("language_id", 2); subtitle2.put("url", "YOUR_ENGLISH_SUBTITLE_URL"); subtitle2.put("format", "webvtt"); subtitle2.put("sub_id", 1); list.put(subtitle2); subtitleJson.put("list", list); subtitleModel = new SubDesInfoModel(subtitleJson); } catch (JSONException e) { e.printStackTrace(); } int subtitleId = 1; // 预加载的字幕 ID String vid = "YOUR_VIDEO_ID_1"; String videoUrl = "YOUR_VIDEO_URL_1"; String videoCacheKey = TTVideoEngine.computeMD5(videoUrl); // 2. 在播放源中设置字幕源 Model source1 = new DirectUrlSource.Builder() .setVid(vid) .addItem(new DirectUrlSource.UrlItem.Builder() .setUrl(videoUrl) .setCacheKey(videoCacheKey) .build()) .setSubtitleModel(subtitleModel) .setSubtitleId(0) .build(); videoUrl = "YOUR_VIDEO_URL_2"; vid = "YOUR_VIDEO_ID_2"; videoCacheKey = TTVideoEngine.computeMD5(videoUrl); source2 = new DirectUrlSource.Builder() .setVid(vid) .addItem(new DirectUrlSource.UrlItem.Builder() .setUrl(videoUrl) .setCacheKey(videoCacheKey) .build()) .setSubtitleModel(subtitleModel) .setSubtitleId(1) .build(); // 3. 设置预加载策略 StrategyPreloadConfig config = new StrategyPreloadConfig.Builder() .setCount(2) .setSize(800000) .setStartBufferLimit(15) .setStopBufferLimit(5) .build(); StrategyManager.instance().setCustomPreloadConfig(config); // 4. 创建播放源列表 List<StrategySource> list = new ArrayList<>(); list.add(source1); list.add(source2); TTVideoEngine.setStrategySources(list); // 5. 给播放器添加播放当前播放源 ttVideoEngine.setStrategySource(source1); ttVideoEngine.setSubDesInfoModel(subtitleModel); ttVideoEngine.setIntOption(PLAYER_OPTION_SWITCH_SUB_ID, 0); }
在初始化 SDK 前,开启预加载策略 V2 版本,并启用预加载策略。
// 开启预加载策略 V2 版本 StrategyManager.setVersion(StrategyManager.VERSION_2); // 初始化播放器 SDK Env.init(/* 省略初始化代码 */); // 开启并使用预加载策略 TTVideoEngine.enableEngineStrategy(STRATEGY_TYPE_PRELOAD, STRATEGY_SCENE_SMALL_VIDEO); // 设置预加载任务工厂,用于预加载策略内部构建预加载任务 StrategyManager.instance().setPreloadTaskFactory(new PreloadTaskFactory() { @Override public PreloaderVidSubtitleItem createSubtitleVidItem(VidPlayAuthTokenSource vidSource, long preloadSize) { final PreloaderVidSubtitleItem item = PreloadTaskFactory.super.createSubtitleVidItem(vidSource, preloadSize); item.setFetchEndListener(new PreloaderVidSubtitleItemFetchListener() { @Override public void onGetPlayInfoResult(VideoModel videoModel, Error error) { // 若支持用户切换视频清晰度,需要通过 item.setResolution 方法重新设置清晰度,覆盖 VidPlayAuthTokenSource 中的清晰度 if (videoModel == null) return; Resolution resolution = Resolution.High; // 480P resolution = TTVideoEngine.findDefaultResolution(videoModel, resolution); final VideoInfo videoInfo = videoModel.getVideoInfo(resolution, false); if (videoInfo == null) return; item.setResolution(videoInfo.getResolution()); } @Override public void onGetSubtitleInfoResult(SubDesInfoModel subDesInfoModel, Error error) { // 若下发多个语言的字幕文件,则需要根据语言来查找对应的字幕 id,通过 item.setSubtitleId 设置后才能预加载对应语言字幕 if (subDesInfoModel == null) return; final List<SubModelProvider> subInfos = subDesInfoModel.getSubModelList(); if (subInfos == null || subInfos.isEmpty()) return; // 语言 ID 枚举参考官网文档:https://www.volcengine.com/docs/4/1186356 final int languageId = 1; // 用户默认选中字幕语言,比如:中文 Integer subtitleId = findPreloadSubtitleIdByLanguageId(subInfos, languageId); if (subtitleId != null) { item.setSubtitleId(subtitleId); } } }); return item; } });
构造用于预加载的 Vid 播放源时,除了视频的 vid 和 playAuthToken,还需提供字幕鉴权 token。
//preload为true时设置使用预加载,id用于选择播放哪个视频 private void setVidSubtitleResource(boolean preload, int id){ String vid = "填写第一个视频的vid"; // AppServer 下发 String playAuthToken = "添加第一个视频的播放token"; // AppServer 下发 String subAuthToken = "添加第一个视频的字幕token"; // AppServer 下发 String vid1 = "填写第二个视频的vid"; // AppServer 下发 String playAuthToken1 = "添加第二个视频的播放token"; // AppServer 下发 String subAuthToken1 = "添加第二个视频的字幕token"; // AppServer 下发 final Resolution resolution = Resolution.High; // 设置 Vid 播放源 VidPlayAuthTokenSource vidPATSource = new VidPlayAuthTokenSource.Builder() .setVid(vid) .setPlayAuthToken(playAuthToken) .setResolution(resolution) .setSubtitleAuthToken(subAuthToken) .build(); VidPlayAuthTokenSource vidPATSource1 = new VidPlayAuthTokenSource.Builder() .setVid(vid1) .setPlayAuthToken(playAuthToken1) .setResolution(resolution) .setSubtitleAuthToken(subAuthToken1) .build(); if(preload){ //设置预加载策略 StrategyPreloadConfig config = new StrategyPreloadConfig.Builder() .setCount(2) .setSize(800000) .setStartBufferLimit(15) .setStopBufferLimit(5) .build(); StrategyManager.instance().setCustomPreloadConfig(config); List<StrategySource> list = new ArrayList<>(); list.add(vidPATSource); list.add(vidPATSource1); //添加预加载播放源列表 TTVideoEngine.setStrategySources(list); } if(id == 1){ ttVideoEngine.setStrategySource(vidPATSource); ttVideoEngine.setSubAuthToken(subAuthToken); }else{ ttVideoEngine.setStrategySource(vidPATSource1); ttVideoEngine.setSubAuthToken(subAuthToken1); } // 设置播放源获取成功回调 ttVideoEngine.setVideoInfoListener(new VideoInfoListener() { @Override public boolean onFetchedVideoInfo(VideoModel videoModel) { // 获取视频数据成功回调 Log.v("RunjieTest", "onFetchedVideoInfo " + videoModel); if (videoModel == null) return false; if (ttVideoEngine == null) return false; // 请求中、英字幕文件的 ID 数组 List<String> subtitleIds = new ArrayList<>(); // 获取当前所有字幕语言的接口 List<SubInfo> subInfoList = ttVideoEngine.supportedSubInfoList(); // 字幕语言映射表见:https://www.volcengine.com/docs/4/1186356 if (subInfoList != null && subInfoList.size() > 0) { for (SubInfo info : subInfoList) { if (info.getValueInt(VALUE_SUB_INFO_LANGUAGE_ID) == 1) { subtitleIds.add(""+info.getValueInt(VALUE_SUB_INFO_ID)); // CN } else if (info.getValueInt(VALUE_SUB_INFO_LANGUAGE_ID) == 2) { subtitleIds.add(""+info.getValueInt(VALUE_SUB_INFO_ID)); // EN } } } String subIds = trans(subtitleIds); // 省略实现,将 subtitleIds 转为逗号分割的字符串,如:"1111,2222" // 传入字幕语言 ID 列表 ttVideoEngine.setStringOption(PLAYER_OPTION_SUB_IDS, subtitleIds.get(0)); return false; } }); }
使用字幕预加载必须要开启数据加载模块 MDL 来加载外挂字幕。
// 0: 不使用 MDL 加载外挂字幕;1: 使用 MDL 加载外挂字幕 mVideoEngine.setIntOption(PLAYER_OPTION_SUB_ENABLE_MDL, 1);
指定播放的字幕 ID。确保播放时设置的字幕 ID 与预加载播放源中的字幕 ID 一致。
mVideoEngine.setIntOption(PLAYER_OPTION_SWITCH_SUB_ID, sub_id);
监听命中回调:通过 VideoEngineInfoListener 监听 USING_MDL_HIT_CACHE_SIZE_SUBTITLE 事件。通过判断 cacheSize 是否大于 0,可以确认是否命中了字幕缓存。若未命中,请检查预加载任务是否成功,以及播放和预加载的 subtitleId 是否匹配。
// TTVideoEngine 实例设置监听 mVideoEngine.setVideoEngineInfoListener(new VideoEngineInfoListener() { @Override public void onVideoEngineInfos(VideoEngineInfos videoEngineInfos) { if (videoEngineInfos == null) { return; } // 字幕缓存命中监听 if (videoEngineInfos.getKey().equals(USING_MDL_HIT_CACHE_SIZE_SUBTITLE)) { // 起播命中缓存判断 String taskKey = videoEngineInfos.getUsingMDLPlayTaskKey();// 使用的 key 信息 long cacheSize = videoEngineInfos.getUsingMDLHitCacheSize();// 命中缓存文件 size // cacheSize > 0, 表示命中缓存 // cacheSize = 0, 未命中缓存,请排查预加载任务是否成功,如果字幕描述信息中包含多个字幕语言,请查看播放和预加载的 subtitleId 是否一致 } } });
在播放过程中,可以通过 addAdditionSubModel 或 switchAdditionSubModel 方法动态地添加新的字幕源并按需切换。
说明
动态添加字幕源自 1.47.1.10 起支持。
// 初始化JSON字幕信息 try { JSONObject subJson = new JSONObject(); subJson.put("sub_id", 1); //(必填)字幕 ID,用于在播放器中区分不同的字幕 subJson.put("language", "cmn-Hans-CN");//(必填)字幕语言 subJson.put("language_id", 1);//(必填)语言 ID subJson.put("url","https://example.srt"); //(必填)字幕文件 URL subJson.put("format", "srt");//(必填)字幕格式,支持 webvtt 和 srt // 添加字幕信息 ttVideoEngine.addAdditionSubModel(new SubModel(subJson)); } catch (JSONException e) { e.printStackTrace(); } // 添加字幕源后,不会主动展示,您需要调用以下接口传入字幕对应的sub_id展示添加后的字幕 ttVideoEngine.setIntOption(PLAYER_OPTION_SWITCH_SUB_ID, sub_id);
// 初始化 JSON 字幕信息 try { JSONObject subJson = new JSONObject(); subJson.put("sub_id", 1); //(必填)字幕 ID,用于在播放器中区分不同的字幕,需要保证字幕ID的唯一性 subJson.put("language", "cmn-Hans-CN");//(必填)字幕语言 subJson.put("language_id", 1);//(必填)语言 ID subJson.put("url","https://example.srt"); //(必填)字幕文件 URL subJson.put("format", "srt");//(必填)字幕格式,支持 webvtt 和 srt // 添加字幕源,并且切换到展示该字幕 ttVideoEngine.switchAdditionSubModel(new SubModel(subJson)); } catch (JSONException e) { e.printStackTrace(); }
JSON 字幕信息包含以下字段:
字段 | 说明 |
|---|---|
sub_id | (必填)字幕 ID,用于在播放器中区分不同的字幕。
|
language_id | (必填)语言 ID。取值详见语言。您需要自行维护字幕 ID 与语言 ID 的映射关系。 |
language | (必填)语言。取值详见语言。 |
url | (必填)字幕文件 URL。 |
format | (必填)字幕格式,支持 |
示例如下:
{ "list": [ { "language": "rus-RU", "language_id": 6, "url": "https://example.volcengine.com/cbebedaade0947ce51a*******17f0b13/6087d12f/video/tos/cn/tos-cn-o-0004/52ce3882d70941d5b660913cbd83d969/", "format": "webvtt", "sub_id": 328934091 }, { "language": "cmn-Hans-CN", "language_id": 1, "url": "https://example.volcengine.com/93adb942***bdfce8cb/6087d12f/video/tos/***a4122dac42d69e8233a4dfda82fe/", "format": "webvtt", "sub_id": 429984091 }, { "language": "cmn-Hans-CN|eng-US", "language_id": 5, "url": "https://***.com/d782d36702***7b9719/6087d12f/video/tos/***5f0d106146a19ad566b967211091/", "format": "webvtt", "sub_id": 829987091 } ] }