586 lines
30 KiB
Markdown
586 lines
30 KiB
Markdown
# 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 五个用例。
|