对于包含多种清晰度、多语言音轨或多语种字幕的视频,播放器 SDK 支持在播放的各个阶段进行精确的管理和切换。本文详细介绍多路流的实现方案,并指导您如何完成集成,以及如何优化切换体验。
播放器 SDK 支持自研协议与标准协议两大类多路流方案,您可以根据业务场景和媒资来源灵活选择。
协议类型 | 播放模式 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
自研协议 | Vid 模式 | 客户端通过 Vid 请求,由视频点播服务端智能下发包含所有可用清晰度的 VideoModel。 | 接入最简单,且安全性强。客户端逻辑清晰,服务端自动处理降级和媒资关联,支持帧对齐平滑切换。 | 依赖视频点播服务,对音轨/字幕的底层控制能力有限。 | 适用于媒资全部托管在火山引擎视频点播服务的场景。 |
VideoModel 模式 | 由您的业务服务端自行构造或从点播服务获取完整的 VideoModel,并下发给客户端。 | 性能最优,减少了一次客户端到点播服务的网络请求,首帧更快。对多路流的管理方式与 Vid 模式一致。 | 需自行实现构造 VideoModel 的逻辑,复杂度稍高。 | 对首帧性能有极致要求的场景,例如短视频信息流。 | |
标准协议 | HLS (Master M3U8) | 客户端直接播放标准的 Master M3U8 文件。SDK 解析文件后,通过底层回调和 Option 设置,允许您对视频流、音轨、字幕进行精细化控制。 | 兼容性最强,适用于任意来源的 HLS 视频流。提供了最底层的控制能力,灵活性最高。 | 客户端实现逻辑最复杂,需要自行处理流信息的遍历、决策和切换。 | 需要播放第三方 HLS 视频流,或需要对音轨、字幕进行精细化管理的场景。 |
DASH | 建设中,敬请期待 | ||||
对于客户端的多清晰度管理,Vid 模式和 VideoModel 模式共享一套简单、直观的 API。
在进行多路流相关开发前,请确保已完成以下准备工作:
PlayAuthToken 时,可不指定 Definition 参数。视频点播服务在响应播放请求时,将默认返回该视频所有可用清晰度的播放地址。在播放器 SDK 触发 onFetchedVideoInfo 回调返回视频播放信息后,调用 supportedResolutionTypes() 获取所有可用的清晰度。
Resolution[] supportedResolutions; ttVideoEngine.setVideoInfoListener(new VideoInfoListener() { @Overridepublic boolean onFetchedVideoInfo(VideoModel videoModel) { if (videoModel != null) { // 获取清晰度数组,用于在 UI 上构建清晰度选择列表 supportedResolutions = ttVideoEngine.supportedResolutionTypes(); } return false; } });
同样在 onFetchedVideoInfo 回调中,您可以根据业务逻辑(如用户历史选择、当前网络状况)设置起播的清晰度。
说明
如果您同时使用了起播选档功能,SDK 会自动选择最佳码率起播,您无需再手动调用 configResolution 设置起播清晰度。
// 在 onFetchedVideoInfo 回调中设置起播 @Override public boolean onFetchedVideoInfo(VideoModel videoModel) { // 业务逻辑:期望以 360p (Standard) 起播 Resolution defaultResolution = Resolution.Standard; // 由于播放源不一定包含 360p,调用 findDefaultResolution 找到最接近的一个可用清晰度 Resolution startResolution = TTVideoEngine.findDefaultResolution(videoModel, defaultResolution); if (startResolution != null) { ttVideoEngine.configResolution(startResolution); } return false; }
当用户在 UI 上选择新的清晰度后,只需调用 configResolution 进行切换,并通过 onVideoStreamBitrateChanged 回调监听切换结果。
// 假设用户从 UI 上点击了 resolutions 列表中的第 i 项 Resolution selectedResolution = supportedResolutions[i]; // 调用接口切换 ttVideoEngine.configResolution(selectedResolution); // 监听切换结果 ttVideoEngine.setVideoEngineCallback(new VideoEngineCallback() { /** * 当码率(清晰度)发生变化时回调 * @param resolution 切换后的清晰度枚举 * @param bitrate 切换后的码率 */ @Override public void onVideoStreamBitrateChanged(Resolution resolution, int bitrate) { // 清晰度切换成功 Log.d("VideoPlay", "Stream switched successfully to: " + resolution + ", bitrate: " + bitrate); } });
Vid 模式下,您可以通过外挂的方式为视频添加和管理多语言字幕。字幕文件与视频 Vid 在视频点播服务端进行绑定。客户端通过 Vid 和 PlayAuthToken 请求播放,再通过 SubtitleToken 请求字幕信息。实现流程如下:
PlayAuthToken 和 SubtitleToken。Vid 和 Token 播放,SDK 内部会自动拉取与该 Vid 关联的所有字幕信息列表。详细集成步骤请参见添加外挂字幕。
对于标准的 HLS Master M3U8 播放源,播放器 SDK 提供底层的控制能力,允许开发者在播放生命周期的不同阶段介入,精确选择要使用的视频和音频流。
一个 Master M3U8 文件就像一个“节目单”,本身不包含视频数据,而是定义了这个节目包含了哪些可用的流。
#EXT-X-STREAM-INF 定义。每个标签都描述了一个特定码率 BANDWIDTH、分辨率 RESOLUTION 的视频流,并指向其对应的子 M3U8 文件。TYPE=AUDIO)或字幕(TYPE=SUBTITLES)。由 #EXT-X-MEDIA 定义。GROUP-ID 属性进行关联。一个视频流可以指定它属于哪个音频组和字幕组,从而决定了在该清晰度下有哪些可选的音轨和字幕。在以下 M3U8 文件示例中,1080p 和 720p 两个清晰度都关联了名为 audio_group 的音频组,这意味着无论在哪种清晰度下,用户都可以选择“英文”或“中文”音轨。
#EXTM3U # --- 定义媒体流 (音轨) --- #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_group",NAME="English",LANGUAGE="en",URI="audio_en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_group",NAME="Chinese",LANGUAGE="zh",URI="audio_zh.m3u8" # --- 定义视频流 (清晰度),并关联音轨 --- #EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,AUDIO="audio_group" video_1080p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,AUDIO="audio_group" video_720p.m3u8
您可以通过设置 setPlayerHLSChooseStreamCallback 回调,在播放器 SDK 解析完 Master M3U8 文件后、正式开始下载媒体数据前,介入并指定使用哪个视频流和音频流进行起播。以下示例代码展示了如何按 720p 分辨率和英文音轨起播:
private String mHlsAudioLanguage = "en"; // 目标音轨语言 private int mVideoStartupResolution = 720; // 目标视频分辨率 private TTVideoEngineMasterPlaylist mMasterPlaylist; private int mHlsVideoVariantIndex; private int mHlsAudioRenditionInfoId; videoEngine.setPlayerHLSChooseStreamCallback(new TTVideoEnginePlayHLSChooseStreamCallback() { // 当 M3U8 解析完成后触发,返回包含所有视频流 variantStreams 和音频流 renditions 的 masterPlaylist 对象。您需要在此方法中遍历并决策出要使用的流的索引或 ID。 @Override public void streamInfos(TTVideoEngineMasterPlaylist ttVideoEngineMasterPlaylist) { // 1. 遍历视频流,找到最匹配目标分辨率的 videoVariant mMasterPlaylist = ttVideoEngineMasterPlaylist; L.d(this, "streamInfos " + new Gson().toJson(mMasterPlaylist)); for (int i = 0; i < ttVideoEngineMasterPlaylist.variantStreams.length; i++) { TTVideoEngineVariantStream videoVariant = ttVideoEngineMasterPlaylist.variantStreams[i]; if (videoVariant.resolution.contains(mVideoStartupResolution + "x")) { // 简单演示用 720P 起播, 业务代码中可以根据业务情况写成一个范围比如:[700, 720] 具体看源是否规整 mHlsVideoVariantIndex = i; break; } } if (mHlsVideoVariantIndex == -1) { mHlsVideoVariantIndex = 0; } } // 播放器调用此方法获取您决策出的视频流带宽。您需要返回 streamInfos 中选定流的带宽值 @Override public int chooseVariantBandWidth() { // 2. 返回第一步中找到的 videoVariant 的带宽 if (mMasterPlaylist == null) { return -1; } if (mMasterPlaylist.variantStreams == null || mMasterPlaylist.variantStreams.length == 0) { return -1; } if (mHlsVideoVariantIndex >= mMasterPlaylist.variantStreams.length) { return -1; } TTVideoEngineVariantStream videoVariant = mMasterPlaylist.variantStreams[mHlsVideoVariantIndex]; if (videoVariant == null) { return -1; } L.d(this, "chooseVariantBandWidth ", new Gson().toJson(videoVariant)); return videoVariant.bandwidth; } // 播放器调用此方法获取您决策出的音轨 ID。您需要返回匹配的音轨 ID @Override public int chooseRenditionInfoId(int videoVariantIndex) { // 3. 根据第一步中找到的 videoVariant 的 audioGroupId // 在所有音轨中查找语言匹配的音轨,并返回其 infoId L.d(this, "chooseRenditionInfoId " + videoVariantIndex); if (mMasterPlaylist == null) { return -1; } if (mMasterPlaylist.variantStreams == null || mMasterPlaylist.variantStreams.length == 0) { return -1; } if (videoVariantIndex >= mMasterPlaylist.variantStreams.length) { return -1; } TTVideoEngineVariantStream videoVariant = mMasterPlaylist.variantStreams[videoVariantIndex]; if (videoVariant == null) { return -1; } if (mMasterPlaylist.renditions == null || mMasterPlaylist.renditions.length == 0) { return -1; } for (int i = 0; i < mMasterPlaylist.renditions.length; i++) { TTVideoEngineMasterPlaylist.TTVideoEngineRendition rendition = mMasterPlaylist.renditions[i]; if (TextUtils.equals(videoVariant.audioGroupId, rendition.groupId) && TextUtils.equals(rendition.type, "AUDIO")) { if (TextUtils.equals(rendition.language, mHlsAudioLanguage)) { mHlsAudioRenditionInfoId = rendition.infoId; return mHlsAudioRenditionInfoId; } } } return -1; } });
在播放过程中,您可以通过设置 PLAYER_OPTION 动态切换音视频流。
要切换视频清晰度,您需要获取到目标清晰度流的带宽值,并通过 setIntOption 设置。
// 假设 mMasterPlaylist 是您在 streamInfos 回调中保存的对象 // 假设用户选择了第 i 个视频流 TTVideoEngineVariantStream targetStream = mMasterPlaylist.variantStreams[i]; int targetBandwidth = targetStream.bandwidth; // 设置 option 来切换 videoEngine.setIntOption(PLAYER_OPTION_SET_MASTER_M3U8_VIDEO_BANDWIDTH, targetBandwidth); // 监听切换结果 videoEngine.setVideoEngineCallback(new VideoEngineCallback() { @Override public void onVideoStreamBitrateChanged(Resolution resolution, int bitrate) { // 对于 Master M3U8 播放,切换成功后 resolution 可能为 null, // 您应该以 bitrate 为准,判断是否切换到了目标带宽。 Log.d("VideoPlay", "Switched to bitrate: " + bitrate); } });
切换音轨则需要获取目标音轨的 infoId。
// 假设用户选择了第 j 个音轨 TTVideoEngineRendition targetRendition = mMasterPlaylist.renditions[j]; int targetInfoId = targetRendition.infoId; videoEngine.setIntOption(PLAYER_OPTION_SET_AUDIO_INFO_ID, targetInfoId);
以下代码展示了如何根据 masterPlaylist 中的信息,动态生成一个可供用户点击切换的清晰度和音轨列表。建议在 streamInfos 回调触发后调用此方法来构建或更新您的 UI。
// 假设 mHlsVideoVariants 是从 MasterPlaylist 解析出的视频流列表 // 假设 context 是当前的上下文环境 // --- 1. 构建清晰度选择 UI --- LinearLayout videoResolutionSelectLayout = new LinearLayout(context); // 遍历所有可用的视频流 for (int i = 0; i < mHlsVideoVariants.size(); i++) { // 获取单个视频流对象 final HLSChooseBestStream.Variant videoVariant = mHlsVideoVariants.get(i); // 创建一个 TextView 用于显示清晰度 final TextView textView = new TextView(context); // 设置显示的文本,例如 "1280x720"。您可以根据业务需求映射为“高清”、“超清”等。 textView.setText(videoVariant.mWidth + "x" + videoVariant.mHeight); // 将 TextView 添加到布局中 videoResolutionSelectLayout.addView(textView); // 为每个清晰度选项设置点击事件监听器 textView.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { // 当用户点击时,记录下所选的清晰度索引 mHlsVideoVariantIndex = i; // 调用 setIntOption,传入目标视频流的带宽,以执行切换 videoEngine.setIntOption(TTVideoEngine.PLAYER_OPTION_SET_MASTER_M3U8_VIDEO_BANDWIDTH, videoVariant.mBandWidth); } }); } // --- 2. 构建音轨选择 UI --- LinearLayout audioLanguageSelectLayout = new LinearLayout(context); // 获取当前正在播放的视频流对象 HLSChooseBestStream.Variant videoVariants = mHlsVideoVariants.get(mHlsVideoVariantIndex); // 遍历当前视频流关联的播放列表 for (int i = 0; i < videoVariants.mPlaylists.length; i++) { HLSChooseBestStream.Playlist playlist = videoVariants.mPlaylists[i]; // 遍历播放列表中的所有 Rendition (通常包含音轨、字幕等) for (int j = 0; j < playlist.mRenditions.length; j++) { HLSChooseBestStream.Rendition rendition = playlist.mRenditions[i]; // 判断当前 Rendition 是否为音轨 if (rendition != null && rendition.mMediaTrackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { // 创建一个 TextView 用于显示音轨信息 final TextView textView = new TextView(context); // 设置显示的文本,例如 "音轨:en"。您可以根据业务需求映射为“英文”、“中文”等。 textView.setText("音轨:" + rendition.mLanguage); // 将 TextView 添加到布局中 audioLanguageSelectLayout.addView(textView); // 为每个音轨选项设置点击事件监听器 textView.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { // 当用户点击时,记录下所选音轨的 infoId mHlsAudioRenditionInfoId = rendition.mInfoId; // 调用 setIntOption,传入目标音轨的 infoId,以执行切换 videoEngine.setIntOption(TTVideoEngine.PLAYER_OPTION_SET_AUDIO_INFO_ID, rendition.infoId); } }); } } }
如果您使用 SDK 提供的预加载策略,同样可以介入并指定预加载哪些流,以优化起播速度和节省带宽。通过设置 setPreloadChooseUrlCallback,您可以在预加载任务解析完 M3U8 文件后,返回一个包含具体流 URL 和预加载大小的列表。以下代码展示了如何预加载 720p 视频流及其关联的音频流:
// 设置预加载选档回调 TTVideoEngine.setPreloadChooseUrlCallback(new TTVideoEnginePreloadHLSChooseUrlCallback() { /** * 在此回调中实现自定义的预加载选档逻辑。 * @param masterPlaylist 解析后的 Master M3U8 对象,包含所有流信息。 * @return 返回一个列表,告知预加载模块具体要缓存哪些流。 */ @Override public List<TTVideoEnginePreloadUrlInfo> chooseUrls(TTVideoEngineMasterPlaylist masterPlaylist) { Log.v("VideoPlay", "chooseUrls:" + new Gson().toJson(masterPlaylist)); if (masterPlaylist.variantStreams == null) { return null; } List<TTVideoEnginePreloadUrlInfo> urlInfos = new ArrayList<>(); for (int i = 0; i < masterPlaylist.variantStreams.length; i++) { TTVideoEngineMasterPlaylist.TTVideoEngineVariantStream videoVariantStream = masterPlaylist.variantStreams[i]; // 1. 根据业务需求(如分辨率)找到要预加载的目标视频流 if (videoVariantStream != null && videoVariantStream.resolution.equals("720x1280")) { TTVideoEnginePreloadUrlInfo videoUrlInfo = new TTVideoEnginePreloadUrlInfo(); videoUrlInfo.uri = videoVariantStream.uri; videoUrlInfo.preloadSize = 500 * 1024; // 设置预加载大小 urlInfos.add(videoUrlInfo); if (masterPlaylist.renditions != null) { for (int j = 0; j < masterPlaylist.renditions.length; j++) { TTVideoEngineMasterPlaylist.TTVideoEngineRendition rendition = masterPlaylist.renditions[j]; if (rendition != null) { // 2. 查找与视频流关联的音轨 (类型为 AUDIO 且 groupId 匹配) if (TextUtils.equals(rendition.type, "AUDIO") && TextUtils.equals(videoVariantStream.audioGroupId, rendition.groupId)) { TTVideoEnginePreloadUrlInfo audioUrlInfo = new TTVideoEnginePreloadUrlInfo(); audioUrlInfo.uri = rendition.uri; audioUrlInfo.preloadSize = 300 * 1024; urlInfos.add(audioUrlInfo); break; } } } } break; } } Log.v("VideoPlay", "[preload] streams " + new Gson().toJson(urlInfos)); return urlInfos; } });
清晰度平滑切换(或称无缝切换)是指播放器在不同清晰度之间切换时,画面过渡平滑、无黑屏、无明显卡顿的技术。
该功能依赖于视频源本身是帧对齐的。
说明
如需了解如何转码生成帧对齐视频以及在 DirectUrl 模式下开启平滑切换功能,可提交工单联系火山引擎技术支持。
通过 setIntOption 方法开启相应的平滑切换开关。
// 开启 HLS 平滑切换的核心开关 ttVideoEngine.setIntOption(TTVideoEngine.PLAYER_OPTION_ENABLE_HLS_SEAMLESS_SWITCH, 1); // 建议同时开启,以获得最佳的 Master M3U8 解析和调度性能 ttVideoEngine.setIntOption(TTVideoEngine.PLAYER_OPTION_ENABLE_MASTER_M3U8_OPTIMIZE, 1); // MP4 播放源平滑切换 ttVideoEngine.setIntOption(PLAYER_OPTION_SEGMENT_FORMAT_FLAG, (1 << SEGMENT_FORMAT_FMP4) | (1 << SEGMENT_FORMAT_MP4)); ttVideoEngine.setIntOption(TTVideoEngine.PLAYER_OPTION_ENABLE_BASH, 1); ttVideoEngine.setVideoEngineCallback(new VideoEngineCallback() { /** * 当码率(清晰度)发生变化时回调 * @param resolution 切换后的清晰度枚举 * @param bitrate 切换后的码率 */ @Override public void onVideoStreamBitrateChanged(Resolution resolution, int bitrate) { Log.d("VideoPlay", "Stream switched successfully to: " + resolution + ", bitrate: " + bitrate); } });