【openKylin 读屏实测 · 第2篇】speech-dispatcher SSIP 协议实战:连接、实时参数与"下一句才变"的语义

【openKylin 读屏实测 · 第2篇】speech-dispatcher SSIP 协议实战:连接、实时参数与"下一句才变"的语义

专栏:《openKylin 3.0 无障碍开发实测》· 第 2 篇(共 9 篇:1 篇总序 + 8 篇专题)
实测环境:openKylin Desktop 3.0(UKUI 4.24 / kylin-wlcom 1.3.1-ok36 / Wayland 会话)、speech-dispatcher 0.12.1-ok1、输出模块 sd_kylin_speech、麒麟端侧引擎 libkylin-ondevice-speech-engine 1.2.0.0
实测时间:2026 年 9 月
版本声明:本文结论均绑定上述版本与真机实测;命令中的 uid、会话号以各自机器实际值为准;标注「待核实」处为尚未定论项,不代表其它版本或架构。
代码说明:文中片段均取自作者自研的开源屏幕阅读器项目(GPL-3.0+),表意伪代码处会显式标注,可自由参考复现。

摘要:在 openKylin 3.0(speech-dispatcher 0.12.1 + sd_kylin_speech 输出模块)上直连 SSIP 协议做了一组判别实验:SET 参数只影响后续 utterance(当前句不变)、每次发声约 2.3s 引擎启动延迟、cancel <0.4s 生效、高优先级打断后低优先级内容自动断点续播(RESUME)、SET PITCH 实测无效。附录音取证方法与项目自有代码。2026-09-23 接入实体声卡后补测:CANCEL 声学尾延 121ms;END/BEGIN 事件在新建连接上可正常收到、仅长跑连接退化——结论已按新证据更新(见 §4、§7 更新块)。


1. 背景

openKylin 3.0 的 TTS 链路是:应用 → speech-dispatcher(0.12.1-ok1)→ sd_kylin_speech 输出模块 → 麒麟端侧引擎(libkylin-ondevice-speech-engine 1.2.0.0)。对读屏来说,语音会话层是"唯一播报通路",它直接决定:参数能不能即时生效、打断后怎么办、连续播报会不会互相截断。这些问题的答案不能靠读文档猜,必须用实验定案。

实测环境:openKylin 3.0 真机,speech-dispatcher 0.12.1 + sd_kylin_speech 模块(模块文件 171KB 在位),会话为 Wayland / kylin-wlcom。

1.1 SSIP 协议速览

SSIP(Speech Synthesis Interface Protocol)是 speech-dispatcher 的文本协议,一行命令 + 三字符响应码:

命令 作用 实测响应示例
SPEAK <priority> <ssml> 发声 225 OK MESSAGE QUEUED
CANCEL 停止当前发声 200 OK CANCEL
SET RATE <n> / SET PITCH <n> / SET VOLUME <n> 参数(-100~100) 200 OK PARAMETER SET
PAUSE / RESUME 暂停/恢复 200 OK
SET SELF NOTIFICATION ALL on 开启事件通知 220 OK NOTIFICATION SET
HISTORY GET 历史查询 只返回历史,不支持实时事件

libspeechd 是它的 C 客户端库,spd-say 是自带命令行测试客户端。

2. 直连方法

2.1 命令行冒烟(spd-say)

# speech-dispatcher 状态与输出模块
systemctl --user status speech-dispatcher || pgrep -a speech-dispatcher
ls /usr/lib/*/speech-dispatcher-modules/   # 预期看到 sd_kylin_speech

# 直接出声(spd-say 是 speech-dispatcher 自带测试客户端)
spd-say -o sd_kylin_speech "你好,麒麟读屏测试"

# 参数即时生效(说话过程中改语速)
spd-say -o sd_kylin_speech -r 30 "这是一段用于测试语速调整的较长文本"
spd-say -o sd_kylin_speech -r 100 "这是一段用于测试语速调整的较长文本"

# 停止当前朗读
spd-say -C

2.2 编程式直连(libspeechd)

产品代码用 libspeechd 以 SPD_MODE_THREADED 模式建立连接。注意两个细节:spd_open 的客户端名是总线可见的;完成回调(callback_end)通过全局槽位转投实例,避免 C 函数指针与 C++ 对象的生命周期错配。

// speech_session.cpp(连接与回调,节选)
bool SpeechSession::connect() {
    if (conn_) return true;
    SPDConnection *c = spd_open("ukui-screen-reader", "main", nullptr, SPD_MODE_THREADED);
    if (!c) return false;
    conn_ = static_cast<void *>(c);
    if (finishedCb_) {
        gFinished = [this](int msgId) {
            if (finishedCb_) finishedCb_(msgId);
        };
        c->callback_end = spdEndTrampoline;
        spd_set_notification_on(c, SPD_END);
    }
    return true;
}

void SpeechSession::close() {
    if (conn_) {
        // 先摘回调再关连接:SPD_MODE_THREADED 下 spd_close 后迟到的 SPD_END
        // 通知仍会经全局槽位转投到已析构的 this(use-after-free)
        static_cast<SPDConnection *>(conn_)->callback_end = nullptr;
        spd_close(static_cast<SPDConnection *>(conn_));
        conn_ = nullptr;
    }
    gFinished = {};   // 无条件清空槽位,未连接路径也安全
}

2.3 优先级映射

业务优先级 → SSIP priority 的映射是队列语义的关键一环:

// speech_session.cpp(业务优先级 → SSIP 映射,节选)
int SpeechSession::toSsipPriority(BizPriority p) {
    switch (p) {
        case BizPriority::P0Interrupt: return SPD_IMPORTANT;    // 打断当前
        case BizPriority::P1Interact:  return SPD_MESSAGE;      // 交互结果
        case BizPriority::P2Content:   return SPD_TEXT;         // 内容级(可被恢复)
        case BizPriority::P3Ambient:   return SPD_NOTIFICATION; // 环境级
    }
    return SPD_TEXT;
}

3. 判别实验一:SET 参数生效语义——“下一句才变”

问题:说话过程中改语速/音调/音量,当前正在播的一句会不会立即变?

方法(同一 client):speak 长句 → 1 秒后 SET RATE/PITCH → 用录音判断当前句是否即时变化。

实测结论:SET 只影响后续 utterance,当前正在播的一句不变。这不是缺陷,而是 SSIP 的既定语义——SET 是参数设置命令,不作用于已入队/正在合成的 utterance。

// speech_session.cpp(百分比 → SSIP 映射与参数下发,节选)
int SpeechSession::percentToSsip(int percent) {
    // 约定:百分比 30~300(100=正常)→ SSIP -100~100
    return std::max(-100, std::min(100, percent - 100));
}

bool SpeechSession::setRate(int percent) {
    if (!conn_) return false;
    return spd_set_voice_rate(static_cast<SPDConnection *>(conn_), percentToSsip(percent)) == 0;
}

工程含义:设置界面改语速后,要提示"下一句生效";读屏内部不能在用户调节参数后立即取消重播当前句(那是另一种体验,且会引入额外打断噪声)。这套语义已固化进设置模块:gsettings 的 changed 信号 → 按 key 下发 SSIP SET → 下一次发声生效。

4. 判别实验二:引擎启动延迟与取消延迟

实测数据(parecord 录音 + 波形能量分段):

  • 每次发声约 2.3s 引擎启动延迟:从 spd_say 调用到第一个可听样本,波形上可见约 2.3s 静默段。这对首次/冷启动播报是明显等待,读屏上层必须用队列异步消化,不能同步阻塞等出声。
  • cancel <0.4s 生效:发出 cancel 后 0.4s 内当前语音停止。2026-09-23 实体声卡复测(单一声源环境):从停止操作打点到 SSIP CANCEL 帧发出为 148–192ms(含 SSH 往返),CANCEL 之后无后续 SPEAK 帧,录音中声音的声学尾延约 121ms——打断及时性有了有声链路的实测值。

连带实测发现:TTS 引擎连续发短句时会互相截断句头句尾——每次发声 2.3s 启动延迟 + 连续 speak 互相 cancel,表现为连打按键时播报残缺甚至完全静默。工程对策:语音会话层必须加最小发声间隔与文本合并策略(高频事件合并为一条播报),而不是逐条 speak。

// speech_session.cpp(发声与取消,节选)
int SpeechSession::speak(const std::string &text, BizPriority priority) {
    if (!conn_) return -1;
    // 注意:约 2.3s 引擎启动延迟,调用方(队列)需异步消化
    return spd_say(static_cast<SPDConnection *>(conn_), toSsipPriority(priority), text.c_str());
}

bool SpeechSession::cancel() {
    if (!conn_) return false;
    return spd_cancel(static_cast<SPDConnection *>(conn_)) == 0;
}

5. 判别实验三:优先级打断 = 断点续播(RESUME)

问题:低优先级内容被高优先级打断后,是丢弃、从头重播、还是从断点继续?

方法(两个独立 client + 录音取证):

  1. client A 用 text 优先级播一段低优长句(同句重复 30 次以便做自相关分析);
  2. 4 秒后 client B 用 important 优先级播打断句;
  3. 全程 parecord 录 auto_null.monitor,做波形能量分段 + 零延迟 NCC + 自相关 F0 三项客观分析(本机无声卡,用客观指标替代人耳听判)。

5.1 时间线(绝对时刻与句内相位分开看)

录音波形的绝对时刻:

0.0s            低优句发出
+3.3s           低优起播(含约 2.3s 引擎启动延迟)
+9.1 ~ +14.4s   打断句播放(时长约 5.3s)
+15.05 ~ 20.2s  低优内容自动续播 —— 打断结束后 +0.65s 从断点继续

另有一个独立于绝对时刻的句内相位量(由自相关算出,单位是"句子周期内的位置",不是墙上时间):

句子周期 = 4.75s;低优被打断于句内相位 2.4s 处;
续播段也从同一相位 2.4s 继续(而不是从句首 0.0s 重新开始)

注意:相位 2.4s 不要和上面的绝对时刻并排比较——它描述"播到这句话的几分之几被切断",所以数值可以小于起播时刻 3.3s,二者是两套坐标。

5.2 续播 vs 重播判别(关键数据)

  • 续播段开头与低优句开头零延迟 NCC = 0.062(重播应 ≈1)→ 是断点续播,不是重播;
  • 句子周期(自相关)4.75s,被打断于相位 2.4s → 续播段从相位 2.4s 处继续;
  • 跨 client:低优与打断是两个独立 spd-say 进程 → 恢复是 SD 的全局行为,不依赖同一连接。

5.3 录音取证三件套(无声卡环境的客观判定)

指标 用途 判读
波形能量分段 “有没有声、什么时候出声” 静默段 = 引擎启动延迟;能量跳变 = 打断点
零延迟 NCC “是不是同一段音频” 续播开头 vs 原句开头 NCC≈1 才是重播
自相关 F0 “音调变没变” F0 中位变化 >10% 才算参数生效

工程含义:读屏队列层的打断语义定为 RESUME 断点续播——critical/important 打断后低优内容由 SD 自动续播,队列层不得重复入队或重播该内容,否则会出现"播一半又从头"的听觉噪声。

6. 判别实验四:SET PITCH 无效(实测结论)

问题:音调参数(pitch)经 SSIP 设置后是否真的改变声音?

方法:无背景干扰重跑,speak 两句分别带 SET PITCH +60 与 SET PITCH -60,录音后自相关 F0 分析。

实测结论:SET PITCH 无效——+60 组 F0 中位 248Hz(9 窗) vs -60 组 266Hz(10 窗),比值 0.93,方向相反且差 7%,落在"无效"区间。结论:sd_kylin_speech 模块未把 pitch 参数透传到引擎。

// speech_session.cpp(pitch 设置,节选——保留协议调用,但已知无效)
bool SpeechSession::setPitch(int percent) {
    if (!conn_) return false;
    // 实测:SSIP SET PITCH 未透传(sd_kylin_speech),返回层仍按协议调用
    return spd_set_voice_pitch(static_cast<SPDConnection *>(conn_), percentToSsip(percent)) == 0;
}

降级处理:设置面仍保留 pitch 键(作为设置面单源),但生效性标注"降级";替代通路 SSML prosody(spd-say -x)待测【待核实:SSML prosody 通路是否生效未验证】。

7. 隐蔽的坑:END 事件在长跑连接上会丢失

【2026-09-23 更新,优先阅读】 本节实验完成于无声卡环境,原定案"服务端不推送事件"表述过强。接入实体声卡后的复测表明:新建的 SSIP 连接可以稳定收到 701 BEGIN / 702 END(单条消息约 1.34s 完成回调);但读屏守护进程的长跑老连接仍然只超时、不收事件。结合"重启 speechd 后首个连接有效、长跑后再次失效"的记录,准确定案是:BEGIN/END 推送与有无声卡无关,而是 speech-dispatcher 运行期按连接退化——新连接正常、长跑连接失效。下方三级证伪实验作为排查路径保留;工程对策(不依赖 END、估算时长看门狗兜底)不变,且仍在生产中有效。

开发中遇到"每条播报后 END 事件不到达、完成判定看门狗持续超时"的现象。做了三级证伪实验定位根因(这是本文最值得复用的排查路径):

级别 实验 结果
1 独立 C 探针(SPD_MODE_THREADED + 全量通知,不依赖本项目代码)发一条消息 零回调(连 BEGIN 都没有)
2 切 sd_dummy 输出模块对照 同样零回调(排除"无声卡→引擎不产事件")
3 裸 socket 直连 SSIP:SET SELF NOTIFICATION ALL on → 220 OK NOTIFICATION SET;SPEAK → 225 OK MESSAGE QUEUED,随后监听 15s 服务端一个事件都没推

当时定案:问题在 speech-dispatcher 服务端一侧,与读屏代码、libspeechd、输出模块均无关;精确口径以文首 2026-09-23 更新块为准(运行期按连接退化,而非"任何连接都不推")。旁证与补充:

  • 服务端日志(/run/user/<uid>/speech-dispatcher/log/speech-dispatcher.log)大量 write() error: 错误的文件描述符 + ERROR: Can't report index mark!;
  • 重启 speechd 后首个连接能收到 BEGIN(msg=1 @+2077ms),长跑后再次失效——说明这是服务端运行期状态问题,不是配置缺失;
  • 替代通路也断:QUEUE → 500 ERR INVALID COMMAND、HISTORY GET → 只能查历史、STATE → 不支持;
  • spd-say -w 实测 3ms 秒回——此前用它判定"正常"是假阴性(它根本没等通知);
  • 单测全绿是因为测试 stub 直接回 END,未覆盖真实引擎回调路径。

工程对策:队列完成判定不再依赖 END 事件——用文本估算时长武装看门狗:

估算时长 = 字数 × 250ms ÷ 语速倍率 + 750ms(语速倍率 = rate/100)
         夹在 [800ms, 20000ms],字数按 UTF-8 码点计
例:rate=100 时标定 4.0 字/秒;10 字 ≈ 3.25s
   (10/24/43 字真机实测 3.25/6.94/11.49s,三点线性拟合残差 <0.2s)
END 若真到达仍优先(更准确),未到达则按估算兜底推进

队列节拍因此不再被固定 5s 兜底周期钉死:正常派发按本条文本的估算时长武装看门狗(如 10 字约 3.25s),END 真到达时提前推进,5s 只在估算缺失时作最后兜底。【待核实:2026-09-23 初步有声观察真实语速约 5–6 字/秒、快于标定的 4.0 字/秒——估算偏长即看门狗稍晚,落在"不自掐"的安全侧;正式多点复标待做】

社区反馈:上述"长跑后 BEGIN/END 事件不推送、服务端日志 write() error: 错误的文件描述符 / Can't report index mark!"已整理成独立探针与日志提交到 speech-dispatcher 仓,供维护者定位(本文不替代该 issue,复现命令与原始日志以 issue 为准):
SSIP 事件通知通道在长跑会话后失效(BEGIN/END 不再推送),服务端日志持续出现 write() error: 错误的文件描述符 · Issue #IKHRA9 · openKylin(孵化期)/speech-dispatcher - Gitee.com

8. 出声取证:无声卡环境下的替代证据

本节为 2026-09-17 无声卡环境的排查记录(实体声卡已于 2026-09-23 接入);排查方法与"硬件无关证据"的思路仍然成立,故原文保留。

本机无声卡(/dev/snd 仅 seq/timer),无法做 dB 级录音取证。排查链(2026-09-17 全程留证):

步骤 现象 结论
程序 speak 后录 auto_null.monitor max_volume -91.0 dB(数字静音) 疑似无声
对照 spd-say(已知可用通路) 同为 -91.0 dB 不是自研链路的问题
对照 paplay 播放已知正弦波(wav 自身 -18.1 dB) 仍 -91.0 dB 采集侧失效
换 pw-record、ffmpeg -f pulse 均 -91.0 dB 三种采集方式一致失败
aplay -l / /proc/asound/cards / pactl list cards short 无声卡;/dev/snd 仅 seq、timer 环境根因
加载 snd-aloop 造虚拟声卡 + 设为默认 sink 仍 -91.0 dB 非 sink 选择问题

替代证据(硬件无关):SSIP end 回调已触发且 msg_id 与 speak 返回值一致(end callback: fired=1 msg_id=4)——该 utterance 已被 SD 接收、合成并走到播放终点。叠加更早的出声取证(spd-say 对照 -8.0 dB / 自研通路 -11.6 dB,在有声环境测得),主链路出声判定为可用。

9. 结论与建议

  1. SSIP 参数语义:SET 只影响后续 utterance——UI 提示"下一句生效",代码注释明确此语义,不算缺陷。
  2. 打断语义:RESUME 断点续播是 SD 的全局行为,队列层负责排队/顶替,恢复不重播——这条直接决定读屏的"打断"听感。
  3. 平台级缺陷要绕开:2.3s 引擎启动延迟用队列异步消化;连发短句互相截断用最小发声间隔 + 文本合并解决;SET PITCH 无效降级标注;END 事件在长跑连接上不可靠(新连接正常),用估算时长看门狗兜底。
  4. 录音取证是读屏开发的必备手段:波形能量分段、零延迟 NCC、自相关 F0 这三件套可以客观判定"有没有声、是不是重播、音调变没变",在没有声卡(录音端)的环境下尤其有用。
  5. 无声卡环境要建立硬件无关的替代证据:SSIP end 回调(msg_id 一致)可以作为"出声链路走到播放终点"的客观判据。

一句话总结四个实验结论:SET 下一句才变、发声有 2.3s 启动延迟、打断是断点续播、pitch 无效——这四个事实决定了语音会话层的全部设计。

10. 常见问题

问题 回答
spd-say 出声但程序不出声? 检查连接模式(THREADED)、输出模块是否指定、优先级映射是否正确
说话中间改语速听不出变化? SET 只影响后续 utterance——协议语义,UI 应提示"下一句生效";若持续无效再查 SET 是否下发成功
打断后低优内容没恢复? RESUME 是 SD 全局行为;若内容被队列误判为"已播完"丢弃过则不会恢复——检查队列判定
END 回调一直不来? 先用一条新建连接对照:新连接正常即命中 speech-dispatcher 长跑连接事件退化(重启服务可临时恢复);应用侧用估算时长看门狗兜底,不影响播报(见 §7 更新块)
pitch 调了没反应? 实测 sd_kylin_speech 不透传 pitch(F0 对照),属平台限制,等 SSML prosody 替代通路验证

附录 A:SSIP 命令速查表(实测响应)

命令 实测响应 用途
SPEAK <prio> <text> 225 OK MESSAGE QUEUED 入队发声
CANCEL 200 OK CANCEL 停止当前发声(<0.4s 生效)
SET RATE n / SET PITCH n / SET VOLUME n 200 OK PARAMETER SET 参数(-100~100,只影响后续句)
PAUSE / RESUME 200 OK 暂停/恢复
SET SELF NOTIFICATION ALL on 220 OK NOTIFICATION SET 开启事件通知(新连接可收 BEGIN/END,长跑连接会退化)
HISTORY GET 只返回历史 不用于实时事件
QUEUE 500 ERR INVALID COMMAND 不支持的替代通路

附录 B:文本估算时长公式(UTF-8 安全)

// text_formatter.cpp(估算时长,节选;2026-09-21 真机三点标定)
std::uint64_t estimateSpeechMs(const std::string &text, int ratePercent) {
    // rate=100 实测 4.0 字/秒:墙钟 ≈ 0.75s + 0.25s×字数
    // (10/24/43 字实测 3.25/6.94/11.49s,线性拟合残差 <0.2s)
    // 语速缩放只作用于字时长项;750ms 固定余量不随语速缩放
    const std::size_t chars = utf8CodepointCount(text);   // 按 UTF-8 码点计
    const double rate = std::clamp(ratePercent, 30, 300) / 100.0;
    const double est = static_cast<double>(chars) * 250.0 / rate + 750.0;
    return static_cast<std::uint64_t>(std::clamp(est, 800.0, 20000.0));
}

附录 C:队列侧打断时序(与 RESUME 配合的工程语义)

下面是表意伪代码,只为突出 P0 打断这一条分支:项目里真实公开方法是 enqueue(ev) -> QueueVerdict(返回 PLAY_NOW/ENQUEUED/MERGED/DROPPED_SUPERSEDED 四态之一),并不存在名为 push 的方法。请勿据此片段反推源码签名。

// queue(表意伪代码:打断语义与 RESUME 的对应关系;真实方法名 enqueue)
void PriorityQueue::push(const UnifiedEvent &ev) {
    if (ev.bizPriority == BizPriority::P0Interrupt) {
        // P0:立即打断当前句(SSIP important 语义),低优内容由 SD 断点续播
        // 队列侧不得重复入队/重播该低优内容——SD 会在打断结束后 RESUME
        interrupt(ev);
        return;
    }
    // P1/P2/P3:正常排队(去重/顶替规则见队列设计)
    queue_.push(ev);
}

这条规则直接对应实验三的实测结论:打断后 +0.65s 低优内容自动续播(非丢弃、非重播),队列层如果再把低优内容重新入队,用户会听到"播一半又从头"。

附录 D:优先级与 SSIP 对照速查

业务优先级 SSIP priority 行为语义 实测依据
P0 打断 SPD_IMPORTANT 立即打断当前句 cancel <0.4s 生效
P1 交互 SPD_MESSAGE 正常排队 —
P2 内容 SPD_TEXT 低优,可被恢复(RESUME) 打断后 +0.65s 断点续播
P3 环境 SPD_NOTIFICATION 环境级 —

附录 E:录音取证完整命令与 NCC 脚本

# 开始录制系统输出(无声卡环境:auto_null.monitor 即系统输出)
parecord --device=auto_null.monitor /tmp/ssip_test.wav &
REC_PID=$!

# 发起发声与参数实验
spd-say -o sd_kylin_speech -r 30 "低优先级长句,用于测试打断与续播语义"
sleep 4
spd-say -o sd_kylin_speech -p 60 "高优先级打断句"

# 停止录制
kill -INT $REC_PID; sleep 1

# 客观分析:能量分段
ffmpeg -i /tmp/ssip_test.wav -af "silencedetect=noise=-40dB:d=0.3" -f null - 2>&1 | grep silence
# NCC:截取两段音频用 python 计算零延迟互相关系数(续播 vs 重播判别)
# F0:自相关基频分析(pitch 有效性判别)

NCC 脚本依赖第三方库 numpy(sudo apt install python3-numpy),仅用于线下录音波形分析,读屏产品代码本身零第三方运行时依赖。

# ncc_check.py —— 零延迟互相关系数(重播应≈1,续播应远离 1)
# 依赖:numpy(仅线下分析用)
import numpy as np, sys, wave

def read_wav(path, start_s, dur_s):
    w = wave.open(path); sr = w.getframerate()
    n = int(sr * dur_s); w.setpos(int(sr * start_s))
    raw = w.readframes(n)
    return np.frombuffer(raw, dtype=np.int16).astype(float), sr

a, sr = read_wav(sys.argv[1], float(sys.argv[2]), float(sys.argv[4]))
b, _  = read_wav(sys.argv[1], float(sys.argv[3]), float(sys.argv[4]))
if len(a) > len(b): a = a[:len(b)]
else:               b = b[:len(a)]
ncc = float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
print(f"NCC = {ncc:.3f}  (>0.9 判为重播,远离 1 判为续播)")

实测数据:续播段 vs 低优句开头 NCC=0.062 → 断点续播;若重播应≈1。0.062 同时排除了"从头重播"与"时间对齐误差"两种解释。

待核实清单(诚实边界)

  • SSML prosody 替代通路是否生效(当前 SET PITCH 无效,可能的绕行路);
  • 文本估算时长已有初步有声观察(真实语速约 5–6 字/秒 vs 标定 4.0 字/秒,估算偏保守),正式多点复标待做;
  • 长跑连接 END 退化的触发条件与稳定复现时长(已确认:实体声卡下新建连接事件正常、仅长跑老连接失效;稳定复现路径仍在与社区 issue 同步排查)。

本系列(共 9 篇):00 总序 | 01 AT-SPI 底座 | 02 SSIP 语音(本文) | 03 Wayland 快捷键 | 04 端侧 OCR | 05 系统通知 | 06 事件防抖 | 07 隐私日志 | 08 版本与环境

上一篇:第 1 篇《openKylin 3.0 无障碍底座实测:AT-SPI 事件总线与屏幕阅读器开发实践》 【openKylin 读屏实测・第 1 篇】openKylin 3.0 无障碍底座实测:AT-SPI 事件总线与屏幕阅读器开发实践 - 社区交流 / 技术交流 - openKylin社区论坛| 下一篇:第 3 篇《kylin-wlcom 全局快捷键:Wayland 下为何 /dev/input 自听是正解》 | 返回专栏总序(待发布后补链接)

作者:xie-linfeng-666(openKylin 社区 / Gitee)。原创内容,转载请注明出处与 openKylin 版本口径。

1 个赞