# 5.1 声道改造 — 代码变更清单与复用指南 > 用途:当前验证代码直接改在 main 上(v1.3.32 → v1.3.35),后续需从 main 单独建 5.1 渠道分支重新套用本文档的变更。 > 验证设备:Amlogic Android 14 盒子(172.16.100.166,曾用 184/175),外接功放 HDMI。 > 验证版本:v1.3.35 aishiRemoteRelease,已确认 5.1/2.0 切换、AC3 直通正常。 --- ## 0. 结论速览 | 项 | 结论 | |---|---| | 播放策略 | 三级:AC3 源码直通 > 平台硬解(6ch LPCM) > FFmpeg 软解(下混 2.0);不支持直通时回退普通 1080P AAC 档 | | 渲染器模式 | `EXTENSION_RENDERER_MODE_ON`(平台优先,FFmpeg 仅兜底)——**不能用 PREFER**(会全量软解,直通不可达) | | 系统设置 | 该固件 `encoded_surround_output` 必须设 **0 (AUTO)**,**不能设 2 (ALWAYS)**(详见 §3,这是本次最大的坑) | | 切换生效 | 切 5.1 开关后无需杀 app,重新起播(换集/退出播放页重进)即生效;播放中不热切(5.1 档与 2.0 档是两条 CDN 流) | | CDN 结构 | 档位因片而异。新片:720P×2 + 1080P AAC 2.0 + 1080P H265 AC3 5.1;老片可能只有 720P + 5.1 档 | | 5.1 流形态 | 单 AC3 音轨、**无 2.0 兜底音轨**,不支持直通时必须整条换流(不可同流切音轨) | | 注入原理 | **v1.3.36 关键修复**:`DefaultAudioSink.Builder()` 必须**无参构造**——传 context 时首次 `getFormatSupport()` 会懒注册 AudioCapabilitiesReceiver 用系统 caps **覆盖注入值**(反编译 2.19.1 字节码确认),v1.3.35 的注入从未生效。随 v1.3.35 版本号发布 | | 直通降级 | 注入会话中音频初始化类错误(1000/4001/5001)自动触发:进程级禁用注入 + 重建播放器软解重试(保留进度,一次为限),播放不中断 | | 音量边界 | 直通后 App 无法干预响度(raw 流不解码不混音,响度由接收端解码决定),与软解路径间存在感知音量差——直通固有特性,非缺陷 | --- ## 1. 代码变更清单(10 个文件) ### 1.1 [新增] `app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java` 5.1 能力检测与播放开关的唯一入口。 ```java package com.pukka.jiecheng.utils; import android.content.Context; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.os.Build; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.pukka.libcore.utils.LogUtils; import com.pukka.libcore.utils.SharedPreferenceUtils; /** * 5.1 环绕声能力检测与播放开关。 *

开关(默认开启)关闭时不选 5.1 独立档(H265+AC3_51),播放普通 2.0 档; * 开启时按设备能力自动选择输出路径:AC3直通(HDMI bitstream) > 平台硬解(6ch LPCM) > FFmpeg软解(下混 2.0)。

*

CDN 侧 5.1 档为单 AC3 音轨流(无 2.0 兜底音轨),故能力不足或开关关闭时必须回退普通档 URL。

*/ public final class Surround51Manager { private static final String TAG = "Surround51Manager"; /** SP 键:5.1 环绕声播放开关(设备信息页可切换) */ private static final String SP_KEY_51_ENABLED = "Surround51Enabled"; private static volatile Boolean sAc3Passthrough; private static volatile Boolean sPlatformAc3Decoder; private Surround51Manager() { } /** 5.1 环绕声播放开关,默认开启 */ public static boolean is51Enabled() { return SharedPreferenceUtils.getBoolean(SP_KEY_51_ENABLED, true); } public static void set51Enabled(boolean enabled) { SharedPreferenceUtils.put(SP_KEY_51_ENABLED, enabled); clearCache(); } /** 清除静态缓存(开关切换后需重新检测) */ public static void clearCache() { sAc3Passthrough = null; sPlatformAc3Decoder = null; } /** * 是否优先选用 5.1 独立档:开关开启 且 Android 5.1(API 22) 以上。 * API 22 及以下设备解码能力有限,固定走普通 2.0 档。 */ public static boolean is51Preferred() { return is51Enabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1; } /** 判断 SDK 返回档位是否 5.1 环绕声档(format='1080P 5.1 环绕声') */ public static boolean isSurroundFormat(String format) { String fmt = format == null ? "" : format; return fmt.contains("5.1") || fmt.contains("环绕声"); } /** * 设备是否支持 AC3 源码直通(HDMI/SPDIF bitstream)。 * 结果受系统编码环绕声设置(encoded_surround_output)与当前接入的音频设备能力影响。 */ public static boolean supportsAc3Passthrough(Context context) { if (sAc3Passthrough == null) { try { sAc3Passthrough = AudioCapabilities.getCapabilities(context) .supportsEncoding(C.ENCODING_AC3); } catch (Throwable e) { LogUtils.d(TAG, "supportsAc3Passthrough err:" + e); sAc3Passthrough = false; } } return sAc3Passthrough; } /** 平台是否自带 AC3 硬解码器(如带 Dolby/MS12 授权的芯片),硬解输出 6ch LPCM 由系统按输出能力下混 */ public static boolean hasPlatformAc3Decoder() { if (sPlatformAc3Decoder == null) { boolean found = false; try { for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { // v1.3.36: isAlias() 是 API 29+ 方法,API 28 及以下调用抛 NoSuchMethodError //(腾讯T5/Android 9 实测),会中断整个循环导致检测永远为"无解码器" if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isAlias()) || info.isEncoder()) { continue; } for (String type : info.getSupportedTypes()) { if ("audio/ac3".equalsIgnoreCase(type)) { found = true; break; } } if (found) { break; } } } catch (Throwable e) { LogUtils.d(TAG, "hasPlatformAc3Decoder err:" + e); found = false; } sPlatformAc3Decoder = found; } return sPlatformAc3Decoder; } /** 当前设备 5.1 音频输出路径描述(设备信息页展示用) */ public static String getAudioPathDesc(Context context) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { return "不支持(系统版本过低)"; } if (supportsAc3Passthrough(context)) { return "AC3源码直通"; } if (hasPlatformAc3Decoder()) { return "硬解码"; } return "软解"; } } ``` 注意事项: - `MediaCodecList` 遍历用 `new MediaCodecList(MediaCodecList.ALL_CODECS)` 构造(API 21+);`MediaCodecList.CodecInfo` 不是内部类,正确类型是独立的 `android.media.MediaCodecInfo`。 - `mediaplayertv` 模块不能依赖此类的选流方法(app 模块),但能力检测逻辑被 §1.2 以直接读 SP + 内联方式复用。 ### 1.2 [修改] `mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java` **改动 1(v1.3.33)**:`initPlayer()` 中渲染器模式 `EXTENSION_RENDERER_MODE_PREFER` → `EXTENSION_RENDERER_MODE_ON`。原因:PREFER 让 FFmpeg 渲染器全量优先(含 AC3 软解为 PCM),直通永远不可达;ON 平台优先、FFmpeg 仅兜底。 **改动 2(v1.3.35)**:`initPlayer()` 中 `DefaultRenderersFactory` 改为匿名子类,重写 `buildAudioSink()`,在系统未报告 AC3 但 5.1 开关开启且系统未禁止环绕声时,强制注入含 AC3 的 `AudioCapabilities`。新增 import: ```java import android.provider.Settings; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.pukka.libcore.utils.SharedPreferenceUtils; ``` `initPlayer()` 中创建播放器的完整替换代码: ```java // 增加缓冲区大小 DefaultLoadControl mLoadControl = new DefaultLoadControl.Builder() .setBufferDurationsMs(50000, 50000, 1500, 2000) .build(); if (mRenderersFactory == null) { mRenderersFactory = new DefaultRenderersFactory(mAppContext) { @Override protected AudioSink buildAudioSink(Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams, boolean enableOffload) { AudioCapabilities caps = AudioCapabilities.getCapabilities(context); boolean ac3Supported = caps.supportsEncoding(C.ENCODING_AC3); // 5.1 开关开启且系统未禁止环绕声时,强制注入 AC3 直通能力。 // 修复:HAL 从 PCM 切回 AUTO 有延迟,getDirectPlaybackSupportedEncodings() // 可能返回过时结果,导致 ExoPlayer 不尝试 AC3 直通。 if (!ac3Supported) { boolean surroundEnabled = SharedPreferenceUtils.getBoolean("Surround51Enabled", true); if (surroundEnabled) { int encodedSurround = Settings.Global.getInt( context.getContentResolver(), "encoded_surround_output", 0); // encoded_surround_output: 0=AUTO, 1=NEVER, 2=ALWAYS if (encodedSurround != 1) { LogUtils.d(TAG, "buildAudioSink: 系统未报告AC3,强制注入。" + " encodedSurround=" + encodedSurround); caps = new AudioCapabilities( new int[]{C.ENCODING_PCM_16BIT, C.ENCODING_AC3}, 8); } } } else { LogUtils.d(TAG, "buildAudioSink: 系统已报告AC3直通能力"); } return new DefaultAudioSink.Builder(context) .setAudioCapabilities(caps) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) .setOffloadMode(enableOffload ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED : DefaultAudioSink.OFFLOAD_MODE_DISABLED) .build(); } }.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON); } ExoPlayer mInternalPlayer = new ExoPlayer.Builder( mAppContext, mRenderersFactory, new DefaultMediaSourceFactory(mAppContext), mTrackSelector == null ? mTrackSelector = new DefaultTrackSelector(mAppContext) : mTrackSelector, mLoadControl, DefaultBandwidthMeter.getSingletonInstance(mAppContext), new DefaultAnalyticsCollector(Clock.DEFAULT)) .build(); ``` **改动 3(v1.3.36,随 v1.3.35 版本号发布:修复注入失效 + 自动降级重试)** 字段(类顶部新增): ```java /** 进程级禁用 AC3 caps 注入:直通初始化失败自动降级后置位,本进程内不再注入(设备级失败,重试无意义) */ private static volatile boolean sAc3InjectionDisabled = false; /** 当前播放器实例的 AudioSink 是否注入了 AC3 caps(onPlayerError 降级判定用) */ private volatile boolean mAc3Injected = false; ``` `buildAudioSink()` 相对改动 2 的变化:注入条件追加 `!sAc3InjectionDisabled`;注入成功置 `mAc3Injected = true`;三分支日志(已报告/注入/注入已禁用);**`new DefaultAudioSink.Builder(context)` 改为无参 `new DefaultAudioSink.Builder()`(关键修复)**: ```java if (!ac3Supported && !sAc3InjectionDisabled) { boolean surroundEnabled = SharedPreferenceUtils.getBoolean("Surround51Enabled", true); if (surroundEnabled) { int encodedSurround = Settings.Global.getInt( context.getContentResolver(), "encoded_surround_output", 0); if (encodedSurround != 1) { LogUtils.d(TAG, "buildAudioSink: 系统未报告AC3,强制注入。" + " encodedSurround=" + encodedSurround); caps = new AudioCapabilities( new int[]{C.ENCODING_PCM_16BIT, C.ENCODING_AC3}, 8); mAc3Injected = true; } } } else if (ac3Supported) { LogUtils.d(TAG, "buildAudioSink: 系统已报告AC3直通能力"); } else { LogUtils.d(TAG, "buildAudioSink: 注入已禁用(本进程曾直通失败),走系统 caps"); } return new DefaultAudioSink.Builder() // 无参!传 context 会导致注入被系统 caps 覆盖 .setAudioCapabilities(caps) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) .setOffloadMode(enableOffload ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED : DefaultAudioSink.OFFLOAD_MODE_DISABLED) .build(); ``` `onPlayerError()` 拦截音频初始化类错误自动降级,`degradeAndRetry()` 重建播放器(核心步骤): ```java // onPlayerError 中(notifyOnError 之前): if (mAc3Injected && !sAc3InjectionDisabled) { int code = error.errorCode; if (code == PlaybackException.ERROR_CODE_UNSPECIFIED // 1000 || code == PlaybackException.ERROR_CODE_DECODER_INIT_FAILED // 4001 || code == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED) { // 5001 LogUtils.d(TAG, "onPlayerError: 注入会话音频初始化失败(code=" + code + "),自动降级为软解重试"); sAc3InjectionDisabled = true; // 进程级一次降级,防循环 degradeAndRetry(error); return; } } // degradeAndRetry 核心:记录进度 → 主线程移除监听并 release → // mRenderersFactory = null(下次 initPlayer 重建 sink,因 sAc3InjectionDisabled=true 不再注入)→ // mAc3Injected = false → initPlayer()/setWakeMode/setOptions/initMediaListener → // setMediaSource(mMediaSource)/prepare()/seekTo(resumePos);任意步骤异常才上抛原错误。 ``` 技术依据(反编译 2.19.1 字节码确认): - **注入失效根因**:`DefaultAudioSink` 私有 `getAudioCapabilities()` 在 `receiver==null && context!=null` 时懒创建 `AudioCapabilitiesReceiver` 并 `register()`,用**系统实时 caps 直接覆盖 `audioCapabilities` 字段**。`Builder(context)` 传 context → 注入值只在构造到首次 `getFormatSupport()` 之间短暂存在,从未参与格式判定;**无参 `Builder()`(context=null)注入才持续生效**。 - `AudioCapabilities.getCapabilities()` **无静态缓存**,每次实时查询;API 29+ TV 走 `AudioManager.getDirectPlaybackSupportedEncodings()`,该查询反映 audio policy/HAL 当前运行时状态,在 HAL 模式切换期间会返回过时结果。 - **`PlaybackException` 位置与命名坑**:类在 **exoplayer-common-2.19.1.aar**(2.18 起模块化拆分,不在 exoplayer-core 的 classes.jar 根包);错误码常量命名是 `ERROR_CODE_*`(**非 `ERRORCODE_*`,编译期找不到符号的坑**)。白名单只收音频初始化类错误(1000/4001/5001),防误降级。 - `MediaCodecAudioRenderer` 直通准入 gate(字节码):`audioSink.supportsFormat(format) && MediaCodecUtil.getDecryptOnlyDecoderInfo() != null`(后者查 `audio/raw` decoder)——注入后有 raw decoder 但 policy 无 DIRECT 输出的设备(如部分 MTK)会在 `AudioTrack(ENCODING_AC3)` 初始化时硬失败,这正是降级重试要防的场景。 - `DefaultRenderersFactory.buildAudioSink()` 是 protected,可被子类重写;`DefaultAudioSink.Builder.setAudioCapabilities()` 是官方注入点。 - SP 键 `"Surround51Enabled"` 与 app 模块 `Surround51Manager.SP_KEY_51_ENABLED` 保持一致(mediaplayertv 依赖 libcore,可直接用 `SharedPreferenceUtils`,两模块读同一 `config.xml`)。 ### 1.3 [修改] `app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java` `getVideoUrls2()`(约 L776)与 `getFreeVideoUrls2()`(约 L841)**两处相同改动**: 1. 主循环普通 1080P 匹配排除 5.1 档: ```java }else if (info.getFormat().contains("1080") &&TextUtils.isEmpty(videoUrl_1080) &&!Surround51Manager.isSurroundFormat(info.getFormat())){ // 普通 1080P 档(排除 5.1 档) ``` 2. SDK 回调尾部新增 5.1 覆盖块(原条件 `Build.VERSION.SDK_INT > LOLLIPOP_MR1` 改为开关判断): ```java if (Surround51Manager.is51Preferred()) { for (VideoInfo info : videoInfos) { if (Surround51Manager.isSurroundFormat(info.getFormat())) { videoUrl_1080 = info.getVideoUrl(); // PlayCodeBean 取码同步覆盖(照原 1080 分支写法) break; } } } ``` 日志锚点:`videoUrl_51 applied:`(排障用)。 ### 1.4 [修改] `app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java` **改动 1**:`getDefinitionUrl()` 恒选 1080 档(约 L446): ```java public static String getDefinitionUrl(String videoUrl_1080, String videoUrl_720) { if (!TextUtils.isEmpty(videoUrl_1080)) { return videoUrl_1080; } return videoUrl_720; } ``` **改动 2**:`setData()` 与 `changeData()` 保持"先释放旧播放器再 setVideoPath"链路(`videoViewRelease()` + `releaseError(true)` → `setVideoPath`),否则 `openVideo()` 里 `mMediaPlayer != null` 直接 return,新 URL 不生效。 ### 1.5 [修改] `app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java` 删除 `onDestroy()` 中强制重置清晰度到 720/1080 的逻辑(约 L3014-3020)。原因:与"5.1 档占用 1080 位置"的新模型冲突。 ### 1.6 [修改] `app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java` `initResolution()`(约 L87)恒隐藏 720P 选项: ```java public void initResolution() { tv720.setVisibility(View.GONE); tv1080.setVisibility(videoDetailStatusManager.isHave1080()?View.VISIBLE:View.GONE); // ... } ``` ### 1.7 [修改] `app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java` 上报取码从读 SP `"Definition"` 改为跟随**实际播放档位**,消除 NPE 风险与错报: ```java private PlayCodeBean getCurPlayCode() { int def = videoDetailStatusManager != null ? videoDetailStatusManager.getCurResolutionUrl() : 0; // def 优先当前档位 → 回落 1080 → 回落 720(按原 PlayCodeBean 组装逻辑) } ``` `getCurResolutionUrl()` 在 `VideoDetailStatusManager` 中实现(读 `mVideoView.getUrl()` 与 1080/720 URL 比对,返回实际档位值)。 ### 1.8 [修改] `app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt` 设备信息页新增音频能力显示行 + 5.1 开关行(入口:关于我们 → 长按"我"字 3 秒 → 设备信息页): ```kotlin // 新增字段 var mTvAudioPath: TextView? = null var mLlSurround51: LinearLayout? = null var mTvSurround51Value: TextView? = null // initView() 中 mTvAudioPath = findViewById(R.id.id_tv_audio_path) mTvAudioPath!!.text = Surround51Manager.getAudioPathDesc(this) mLlSurround51 = findViewById(R.id.id_ll_surround51) mTvSurround51Value = findViewById(R.id.id_tv_surround51_value) updateSurround51Value() mLlSurround51?.setOnClickListener { Surround51Manager.set51Enabled(!Surround51Manager.is51Enabled()) updateSurround51Value() ToastUtils.showShortToastSafe( if (Surround51Manager.is51Enabled()) "已开启 5.1 环绕声,重新进入播放生效" else "已关闭 5.1 环绕声,播放普通 2.0 声道" ) } private fun updateSurround51Value() { mTvSurround51Value?.text = if (Surround51Manager.is51Enabled()) "开" else "关" } ``` ### 1.9 [修改] `app/src/main/res/layout/activity_device.xml` 在会员到期行后新增两行:音频能力行(`id_tv_audio_path`)+ 5.1 开关行(`id_ll_surround51` / `id_tv_surround51_value`),开关行带焦点背景 `bg_setting_item_selector`。 ### 1.10 [新增] `app/src/main/res/drawable/bg_setting_item_selector.xml` 焦点态半透明白色背景 selector。 ### 1.11 [修改] `app/build.gradle` `versionName` 递增(本次验证链:v1.3.32 → v1.3.33 → v1.3.34 → v1.3.35)。 依赖(已有,勿动): ```groovy implementation 'com.google.android.exoplayer:exoplayer:2.19.1' implementation files('libs/exoplayer-ffmpeg-extension-2.19.1.aar') ``` `mediaplayertv/build.gradle` 同版本 exoplayer 2.19.1,且依赖 `project(':libcore')`。 --- ## 2. 排障经验(可直接复用的方法论) ### 2.1 问题一:"5.1 关掉后再打开不生效"(v1.3.34 现象:再开只有 2.0) 排查链(每步均有证据支撑): | 假设 | 结论 | 证据 | |---|---|---| | ExoPlayer AudioCapabilities 静态缓存 | 不存在 | 反编译 2.19.1:`getCapabilities()` 每次实时查询 | | ExoPlayer 实例未重建 | 正常重建 | `changeData()` → `videoViewRelease()` → `openVideo()` → `createPlayer()` 每次新建 | | URL 选流错误 | 正常 | logcat `videoUrl_51 applied` 在开关后仍出现 | | `encoded_surround_output` 被重置 | 值仍为 2 | `adb shell settings get global encoded_surround_output` | | HAL 模式未切换 | **根因** | `dumpsys media.audio_flinger` 显示 `digital_audio_mode: PCM` | ### 2.2 问题二:v1.3.35 现象升级为"完全无声" 关键证据: ``` # HAL 日志(logcat) audio_hw_hal_stream: [get_sink_format] format:AC3(0x9000000) digital_mode(PCM) sink cap:DOLBY_TRUEHD audio_hw_hal_primary: [config_output] hal_internal_format:AC3 dolby_lib_type:1 reset_decoder:1 rate=48000 ch=6 # audio_flinger dump:DIRECT 线程有 AC3 流、Frames written 在涨,但全局 digital_audio_mode=PCM ``` 解释:policy 层(ALWAYS)向 app 报告 AC3 能力 → ExoPlayer 走直通 → AC3 raw 流进 DIRECT 线程;但 HAL 全局 `digital_audio_mode=PCM` → HDMI 物理层按 PCM 输出 → 功放收到无法解码的数据 → 无声。 ### 2.3 最终根因(本次最重要的经验) 该 Amlogic 固件 SettingsObserver 对 `encoded_surround_output` 的映射(实验实测矩阵): | encoded_surround_output | HAL digital_audio_mode | 直通可用 | |---|---|---| | **0 (AUTO)** | **AUTO** | **是(正确部署值)** | | 3 (MANUAL,Amlogic 系统设置里的"手动") | AUTO | 是 | | 1 (NEVER) | PCM | 否 | | **2 (ALWAYS,AOSP 标准"总是")** | **PCM** | **否(致命陷阱)** | **ALWAYS=2 时 policy 照常向 app 报告 AC3 能力,但 HAL 被 SettingsObserver 切到 PCM** —— 两层矛盾,表现为无声或静默回退 2.0。**部署必须用 0 (AUTO)**,与 Amlogic 系统设置 UI 里的"自动"选项对应(UI 只提供 PCM/自动/手动,对应 1/0/3)。 另:设备**重启后系统组件会把 `encoded_surround_output` 重置为 1 (NEVER)**(约开机 13 秒后 SettingsObserver 生效)。此时 app 侧安全回退 FFmpeg 软解 2.0,不会无声,但 5.1 失效,需设备侧固化(见 §4 待办)。 ### 2.4 恢复 HAL AUTO 的手段(已验证) 1. 重启设备(HAL 回 AUTO,但 setting 被重置为 1,需再设 0): ```bash adb reboot adb shell settings put global encoded_surround_output 0 # 重启后必须补设 ``` 2. 纯切 setting 不重启:`settings put global encoded_surround_output 0` 即可将 HAL 从 PCM 切回 AUTO(实测有效)。 ### 2.5 CDN 流结构(实测修正) 以《飞驰人生2》playAuth 响应为例,4 档: | 文件名特征 | 视频 | 音频(ffprobe 实测) | format 字段 | |---|---|---|---| | `.mp4.hls` | H264 1.5M 720P | AAC 2.0 | `720P` | | `_h264_1.8m-720p` | H264 1.8M 720P | AAC 2.0 | `720P` | | `_h264_2.5m-1080p` | H264 2.5M 1080P | **AAC 48000Hz 2ch** | `1080P` | | `_1080P_h265_3.5M_SDR_AC3_51` | H265 3.5M 1080P | AC3 5.1(单音轨) | `1080P 5.1 环绕声` | - 早前"1080P 只有 AC3 5.1(隐式耦合)"的结论**是误判**,只适用于部分老片;新片有独立 1080P AAC 2.0 档。 - "5.1 流单 AC3 音轨、无 2.0 兜底"结论**仍成立**——5.1 流内无 2.0 音轨,不支持直通必须整条换 URL。 - 现有选流逻辑天然兼容两种片源结构:关 5.1 → 普通 1080P AAC(老片无此档回落 720P);开 5.1 → AC3 档。 ### 2.6 v1.3.35 注入失效根因(腾讯T5 光纤场景排查实录,v1.3.36 修复) **现象**:腾讯 T5 投影(Amlogic TL1 / Android 9 / API 28),系统声音输出设为光纤 SPDIF RAW,设备页仍显示软解,AC3 走平台解码下混 2.0 PCM。 **排查链(三个 App 侧根因)**: 1. **注入被系统 caps 覆盖(核心)**:`DefaultAudioSink.Builder(context)` 传 context → `getFormatSupport()` 首次调用懒注册 `AudioCapabilitiesReceiver` 覆盖注入值 → **v1.3.35 注入从未生效**(166 的直通是系统 HDMI 自报 AC3,与注入无关;122 软解是平台无 AC3 decoder,也与注入无关)。修复 = 无参 `Builder()`,详见 §1.2 改动 3。 2. **isAlias API 兼容**:`MediaCodecInfo.isAlias()` 是 API 29+ 方法,API 28 抛 `NoSuchMethodError` 被外层 catch 吞掉 → `hasPlatformAc3Decoder()` 循环中断、永远 false。修复 = `SDK_INT >= Q` 守卫。 3. **能力检测口径(注入的价值所在)**:系统音频能力上报(HDMI plug 广播 / `getDirectPlaybackSupportedEncodings()`)**只感知 HDMI,不感知 SPDIF**——光纤场景检测 UI 显示"软解"是口径内必然;而 T5 平台实际支持 AC3 直通(policy 有 tunnel raw profile、注册 `audio/raw` 与 `audio/ac3` decoder)。caps 注入就是让 ExoPlayer 越过系统口径尝试直通,失败再由降级重试兜底。 **降级重试(L2 风险防护)**:注入生效后出现新风险面——有 raw decoder 但 policy 无 DIRECT 压缩输出的设备,`AudioTrack(ENCODING_AC3)` 初始化失败是**运行时硬失败不回落**。方案:错误码白名单(1000/4001/5001)→ 进程级 `sAc3InjectionDisabled` 置位 → 重建播放器软解重试(保留进度、一次为限、重试失败才上抛原错误)。 **验证结论(T5 光纤 SPDIF,v1.3.36 实测)**:注入生效 → `EventLogger` 音轨 `audio/ac3 channels=6 supported=YES` → HAL 日志 `SPDIF_FORMAT 2 + spdif mute 0`(AC3 raw 输出,对比修复前 `nPassThroughEnable:0` 解码模式)→ 客户环境播放出声,直通闭环成立,未触发降级(一次成功)。App 侧新增的可观察日志:`buildAudioSink: 系统未报告AC3,强制注入`。 **版本口径**:修复代码注释标记 v1.3.36,但随 versionName **v1.3.35** 发布(发版版本号未升,versionCode 按 yyMMddHH 自动递增)。 --- ## 3. 验证手册 ### 3.1 设备状态检查 ```bash adb connect 172.16.100.166 adb shell settings get global encoded_surround_output # 期望 0 adb shell dumpsys media.audio_flinger | grep digital_audio_mode # 期望 AUTO adb shell cat /sys/class/audiodsp/digital_raw # 播放 AC3 中期望 1,空闲可能 0 ``` ### 3.2 播放中直通链路验证 ```bash # DIRECT 线程应为 AC3 + RAW_DIRECT + active adb shell dumpsys media.audio_flinger | grep -E "Output thread|HAL format|usecase|active" # 期望: # Output thread ... type 1 (DIRECT) # HAL format: 0x9000000 (AUDIO_FORMAT_AC3) # -usecase: RAW_DIRECT # N Tracks of which M are active # App 侧关键日志 adb logcat -d | grep -E "buildAudioSink|videoUrl_51 applied" # 期望: # buildAudioSink: 系统已报告AC3直通能力(正常路径) # 或 buildAudioSink: 系统未报告AC3,强制注入(注入路径) # videoUrl_51 applied: # 直通初始化失败自动降级时(罕见,Amlogic 光纤直通一次成功的场景看不到): # onPlayerError: 注入会话音频初始化失败(code=...),自动降级为软解重试 # degradeAndRetry: 关闭AC3注入重建播放器, resumePos=... # buildAudioSink: 注入已禁用(本进程曾直通失败),走系统 caps ``` ### 3.3 功能验收用例 1. 开 5.1 播放 → 功放面板显示 AC3、5.1 效果 ✓(v1.3.35 + setting=0 已验收) 2. 关 5.1 重新起播 → 普通档 URL(`_h264_*` 非 AC3_51)、2.0 ✓ 3. 再开 5.1 重新起播 → AC3 档、直通恢复 ✓ 4. 切换仅重新起播即生效,无需杀 app ✓ 5. `encoded_surround_output=1`(NEVER)时播放 → 回退软解 2.0,不无声 ✓(HAL PCM + policy 不报 AC3 + 注入条件排除 NEVER,三者一致) 6. 光纤 SPDIF 设备(系统能力上报不感知 SPDIF):SPDIF RAW 模式 + 5.1 开启 → 注入路径直通 ✓(腾讯T5/Amlogic TL1/Android 9,客户环境实测出声,未触发降级) ### 3.4 CDN 档位探测(排查片源问题用) ```bash # 从 logcat 拿 playAuth response 里的目标档位 URL(含 auth_key),然后: curl -s "" | head -8 # 拿首段 ts 的 auth_key curl -s -o /tmp/probe.ts "" ffprobe -v error -show_entries stream=index,codec_name,codec_type,channels,sample_rate -of csv /tmp/probe.ts ``` --- ## 4. 遗留待办 1. **设备侧固化 `encoded_surround_output=0`**:排查开机把它重置为 1 的系统组件(疑似 Amlogic 设置应用/固件默认逻辑),部署设备需固化,否则每次重启后 5.1 静默降级 2.0。 2. **5.1 节目标识角标**:详情页/海报角标"5.1",基于 `VideoInfo.format` 判断(EPG 无字段)。 3. **清晰度菜单档位名**:5.1 开启时显示"1080P 环绕声"替代直接替换 1080 档文案。 4. (可选)播放器内 5.1 实时状态指示:读取当前流音轨格式显示直通/软解状态。 5. **122 (MTK) 降级重试回归实测**:v1.3.36 注入真正生效后,有 `audio/raw` decoder 的设备将首次尝试 `AudioTrack(AC3)` 直通,预期触发 `degradeAndRetry` 回落软解——降级路径尚未真机验证,装正式包后需顺带回归(日志关键字见 §3.2)。 --- ## 5. 分支重建操作建议 ```bash # 从 main 建 5.1 渠道分支 git checkout main && git pull git checkout -b feature/51-surround # 按 §1 清单逐文件套用(1.1/1.10 为新增文件,其余为修改) # 编译验证 sh gradlew :app:assembleaishiRemoteRelease # 按本机既有命名约定打 AS 包(aishiRemoteRelease 变体) ``` 验收顺序:§3.1 设备状态 → §3.2 直通链路 → §3.3 五个用例。