Files
jiecheng-business/5.1声道改造-代码变更清单与复用指南.md
freeboss b7b5518834 feat: dev-5.1 分支基线(v1.3.35 发版后代码 + 5.1 声道改造文档)
- 以 main(ccc65af) 为基线提交当前工作区代码
- 新增 5.1 声道改造文档两份
- .gitignore 补充 .vscode/、.screenshots/
2026-11-02 19:25:12 +08:00

586 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 环绕声能力检测与播放开关。
* <p>开关(默认开启)关闭时不选 5.1 独立档(H265+AC3_51),播放普通 2.0 档;
* 开启时按设备能力自动选择输出路径:AC3直通(HDMI bitstream) > 平台硬解(6ch LPCM) > FFmpeg软解(下混 2.0)。</p>
* <p>CDN 侧 5.1 档为单 AC3 音轨流(无 2.0 兜底音轨),故能力不足或开关关闭时必须回退普通档 URL。</p>
*/
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:<url>`(排障用)。
### 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<TextView>(R.id.id_tv_audio_path)
mTvAudioPath!!.text = Surround51Manager.getAudioPathDesc(this)
mLlSurround51 = findViewById<LinearLayout>(R.id.id_ll_surround51)
mTvSurround51Value = findViewById<TextView>(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:<AC3_51 档 URL>
# 直通初始化失败自动降级时(罕见,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 "<index.m3u8 URL>" | head -8 # 拿首段 ts 的 auth_key
curl -s -o /tmp/probe.ts "<index0.ts URL>"
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 五个用例。