对于标准的 Master M3U8 播放源,播放器 SDK 提供底层的控制能力,允许开发者在播放生命周期的不同阶段介入,精确选择要使用的视频和音频流。
通过设置 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; } });