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

30 KiB
Raw Permalink Blame History

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 能力检测与播放开关的唯一入口。

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:

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() 中创建播放器的完整替换代码:

// 增加缓冲区大小
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 版本号发布:修复注入失效 + 自动降级重试)

字段(类顶部新增):

/** 进程级禁用 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()(关键修复):

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() 重建播放器(核心步骤):

// 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 档:
}else if (info.getFormat().contains("1080")
        &&TextUtils.isEmpty(videoUrl_1080)
        &&!Surround51Manager.isSurroundFormat(info.getFormat())){
    // 普通 1080P 档(排除 5.1 档)
  1. SDK 回调尾部新增 5.1 覆盖块(原条件 Build.VERSION.SDK_INT > LOLLIPOP_MR1 改为开关判断):
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):

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 选项:

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 风险与错报:

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 秒 → 设备信息页):

// 新增字段
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)。 依赖(已有,勿动):

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):
    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 设备状态检查

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 播放中直通链路验证

# 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 档位探测(排查片源问题用)

# 从 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. 分支重建操作建议

# 从 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 五个用例。