mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-26 13:07:49 -04:00
* bluetooth: identify codecs from the profile description PipeWire describes profiles in the daemon's locale and only names the codec in that description for the bare profile (LHDC settles on "a2dp-sink"), so the old English "codec %s" match dropped LHDC profiles on non-English systems. Parsing the translated shape instead only moves the coupling to word order and punctuation. Emit the profile index and raw description from the lua script and let the UI name the codec by matching the untranslated literal from one codec table. Switch by index, falling back to the profile name or codec literal, so no translated text is parsed. Also teach the BlueZ fallback the Savitech ids (LHDC v5 and v3). Test: quickshell/tests/bluez-codec.test.mjs * bluetooth: show unknown codec without masking bluez * bluetooth: ignore stale codec query results
463 lines
22 KiB
JavaScript
463 lines
22 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
|
|
const read = path => readFileSync(new URL("../" + path, import.meta.url), "utf8");
|
|
|
|
// codecNameFromBluez reads BlueZ's Configuration byte array: the first four
|
|
// bytes are the vendor id, the next two the vendor codec id, both little endian.
|
|
function bluezConfiguration(vendor, codec) {
|
|
const bytes = [];
|
|
for (let i = 0; i < 4; i++)
|
|
bytes.push((vendor >>> (i * 8)) & 0xff);
|
|
bytes.push(codec & 0xff, (codec >>> 8) & 0xff);
|
|
return bytes;
|
|
}
|
|
|
|
const serviceSource = read("Services/BluetoothService.qml");
|
|
|
|
function methods(names) {
|
|
const bodies = names.map(name => {
|
|
const start = serviceSource.indexOf(" function " + name + "(");
|
|
assert.notEqual(start, -1, "BluetoothService.qml lost " + name);
|
|
const end = serviceSource.indexOf("\n }", start) + 6;
|
|
return serviceSource.slice(start, end);
|
|
});
|
|
const context = vm.createContext({ I18n: { tr: text => text } });
|
|
vm.runInContext(bodies.join("\n"), context);
|
|
return context;
|
|
}
|
|
|
|
const luaScript = new URL("../scripts/bluez-card-profile.lua", import.meta.url).pathname;
|
|
|
|
// list mode emits the profile index and the raw description; set mode matches a
|
|
// profile by index, by name, or by an untranslated codec literal passed from the
|
|
// UI. The script never parses the translated description, so word order and
|
|
// punctuation no longer matter.
|
|
const deviceDriver = String.raw`
|
|
local path = arg[1]
|
|
local src = assert(io.open(path)):read("*a")
|
|
local region = assert(src:match("(local function profile_available.-\nend\n)\nom:connect"),
|
|
"bluez-card-profile.lua: profile_available/handle_device region moved")
|
|
|
|
cutils = { parseParam = function(param) return param end }
|
|
Pod = { Object = function(table) return table end }
|
|
Core = { quit = function() end, sync = function(callback) if callback then callback() end end }
|
|
fail = function(message) print("FAIL\t" .. message) end
|
|
dev_name = "test-dev"
|
|
mode = "list"
|
|
target = ""
|
|
args = {}
|
|
handled = false
|
|
|
|
local handle_device = assert(load(region .. "\nreturn handle_device", "handle"))()
|
|
|
|
local function device_with(profiles, active, name)
|
|
return {
|
|
properties = { ["device.name"] = name or "test-dev" },
|
|
iterate_params = function(self, kind)
|
|
local list = profiles
|
|
if kind == "Profile" then
|
|
list = active and { { index = active } } or {}
|
|
end
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return list[i]
|
|
end
|
|
end,
|
|
set_param = function(self, kind, param)
|
|
print(string.format("SET\t%s\t%s", kind, tostring(param.index)))
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function run_list(label, profiles, active)
|
|
mode = "list"
|
|
handled = false
|
|
target = ""
|
|
args = {}
|
|
print("CASE\t" .. label)
|
|
handle_device(device_with(profiles, active))
|
|
print("HANDLED\t" .. tostring(handled))
|
|
end
|
|
|
|
local function run_set(label, requested, requested_index, requested_codec, profiles)
|
|
mode = "set"
|
|
handled = false
|
|
target = requested
|
|
args = { index = requested_index, codec = requested_codec }
|
|
print("CASE\t" .. label)
|
|
handle_device(device_with(profiles))
|
|
print("HANDLED\t" .. tostring(handled))
|
|
end
|
|
|
|
run_list("list", {
|
|
{ name = "a2dp-sink", description = "高保真回放 (A2DP 信宿, 编码 LHDC v5)", index = 3 },
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
{ name = "off", description = "关", index = 0 },
|
|
}, 3)
|
|
|
|
run_set("set-index", "a2dp-sink", 5, "", {
|
|
{ name = "a2dp-sink", description = "高保真回放 (A2DP 信宿, 编码 LHDC v5)", index = 5 },
|
|
})
|
|
run_set("set-name", "a2dp-sink-sbc_xq", nil, "", {
|
|
{ name = "a2dp-sink-sbc_xq", description = "High Fidelity Playback (A2DP Sink, codec SBC-XQ)", index = 4 },
|
|
})
|
|
run_set("set-codec-fi", "", nil, "LHDC v5", {
|
|
{ name = "a2dp-sink", description = "Korkealaatuinen toisto (A2DP-kohde, LHDC v5-koodekki)", index = 7 },
|
|
})
|
|
run_set("set-codec-hu", "", nil, "LHDC v5", {
|
|
{ name = "a2dp-sink", description = "Magas hűségű lejátszás (A2DP fogadó, LHDC v5 kodek)", index = 8 },
|
|
})
|
|
run_set("set-codec-bg", "", nil, "LHDC v5", {
|
|
{ name = "a2dp-sink", description = "Изпълнение с висока точност (елемент-приемник A2DP, кодер „LHDC v5“)", index = 9 },
|
|
})
|
|
run_set("set-codec-my", "", nil, "LHDC v5", {
|
|
{ name = "headset-head-unit", description = "မိုက်ပါနားကြပ်ခေါင်းယူနစ် (HSP/HFP၊ codec LHDC v5)", index = 10 },
|
|
})
|
|
run_set("set-no-match", "", nil, "aptX", {
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
})
|
|
run_set("set-name-over-index", "a2dp-sink-sbc_xq", 1, "", {
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
{ name = "a2dp-sink-sbc_xq", description = "High Fidelity Playback (A2DP Sink, codec SBC-XQ)", index = 2 },
|
|
})
|
|
`;
|
|
|
|
function parseDeviceOutput(output) {
|
|
const cases = {};
|
|
let current = null;
|
|
for (const line of output.split("\n")) {
|
|
const columns = line.split("\t");
|
|
if (columns[0] === "CASE") {
|
|
current = cases[columns[1]] = { codecs: [], set: null, ok: null, fail: null, handled: null };
|
|
} else if (!current) {
|
|
continue;
|
|
} else if (columns[0] === "CODEC") {
|
|
current.codecs.push(columns.slice(1));
|
|
} else if (columns[0] === "SET") {
|
|
current.set = columns[1] + ":" + columns[2];
|
|
} else if (columns[0] === "OK") {
|
|
current.ok = columns[1];
|
|
} else if (columns[0] === "FAIL") {
|
|
current.fail = columns[1];
|
|
} else if (columns[0] === "HANDLED") {
|
|
current.handled = columns[1] === "true";
|
|
}
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
test("list mode reports every available profile by index and raw description", () => {
|
|
const output = execFileSync("lua", ["-", luaScript], { input: deviceDriver, encoding: "utf8" });
|
|
const cases = parseDeviceOutput(output);
|
|
assert.deepEqual(cases.list.codecs, [
|
|
["3", "a2dp-sink", "高保真回放 (A2DP 信宿, 编码 LHDC v5)", "1"],
|
|
["1", "a2dp-sink-sbc", "High Fidelity Playback (A2DP Sink, codec SBC)", "0"],
|
|
["0", "off", "关", "0"],
|
|
]);
|
|
});
|
|
|
|
test("set mode matches a profile by index, name or untranslated codec literal", () => {
|
|
const output = execFileSync("lua", ["-", luaScript], { input: deviceDriver, encoding: "utf8" });
|
|
const cases = parseDeviceOutput(output);
|
|
assert.deepEqual(cases["set-index"], { codecs: [], set: "Profile:5", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-name"], { codecs: [], set: "Profile:4", ok: "a2dp-sink-sbc_xq", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-fi"], { codecs: [], set: "Profile:7", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-hu"], { codecs: [], set: "Profile:8", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-bg"], { codecs: [], set: "Profile:9", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-my"], { codecs: [], set: "Profile:10", ok: "headset-head-unit", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-no-match"], { codecs: [], set: null, ok: null, fail: "profile not found: aptX", handled: true });
|
|
assert.deepEqual(cases["set-name-over-index"], { codecs: [], set: "Profile:2", ok: "a2dp-sink-sbc_xq", fail: null, handled: true });
|
|
});
|
|
|
|
test("a codec is identified from its description in any locale", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "getCodecInfo"]);
|
|
const info = description => context.codecInfoFromDescription(description);
|
|
assert.equal(info("高保真回放 (A2DP 信宿, 编码 LHDC v5)").name, "LHDC v5");
|
|
assert.equal(info("Korkealaatuinen toisto (A2DP-kohde, LHDC v5-koodekki)").name, "LHDC v5");
|
|
assert.equal(info("Magas hűségű lejátszás (A2DP fogadó, LHDC v5 kodek)").name, "LHDC v5");
|
|
assert.equal(info("Изпълнение с висока точност (елемент-приемник A2DP, кодер „LHDC v5“)").name, "LHDC v5");
|
|
assert.equal(info("Слушалки с микрофон (HSP/HFP, кодер „LHDC v5“)<").name, "LHDC v5");
|
|
assert.equal(info("မိုက်ပါနားကြပ်ခေါင်းယူနစ် (HSP/HFP၊ codec LHDC v5)").name, "LHDC v5");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec SBC-XQ)").name, "SBC-XQ");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec SBC)").name, "SBC");
|
|
assert.equal(info("Wiedergabe in hoher Qualität (A2DP Sink, codec AAC-ELD)").name, "AAC-ELD");
|
|
assert.equal(info("Playback (A2DP Sink, codec aptX HD)").name, "aptX HD");
|
|
assert.equal(info("Playback (A2DP Sink, codec aptX-LL)").name, "aptX LL");
|
|
// The exact PipeWire name wins over a shorter one inside it.
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec LC3plus HR)").name, "LC3plus HR");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec LC3-24kHz)").codec, "LC3-24kHz");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec Opus 05 5.1 Surround)").codec, "Opus 05 5.1 Surround");
|
|
assert.equal(info("Playback (A2DP Sink, codec aptX-LL mSBC)").name, "aptX LL");
|
|
assert.equal(info("Hearing aid (ASHA, codec G722)").name, "G722");
|
|
assert.equal(info("Headset (HSP/HFP, codec MSBC)").name, "mSBC");
|
|
assert.equal(info("关"), null);
|
|
assert.equal(info("Auto: Prefer Quality (A2DP)"), null);
|
|
});
|
|
|
|
test("a CODEC line carries the profile index and the codec read from the description", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "codecLabelFromProfile", "getCodecInfo", "parseCodecLine"]);
|
|
const entry = context.parseCodecLine("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 LHDC v5)\t1");
|
|
assert.equal(entry.name, "LHDC v5");
|
|
assert.equal(entry.profile, "a2dp-sink");
|
|
assert.equal(entry.index, 3);
|
|
assert.equal(entry.codec, "LHDC v5");
|
|
assert.equal(entry.current, true);
|
|
assert.equal(context.parseCodecLine("CODEC\t0\toff\t关\t0"), null);
|
|
assert.equal(context.parseCodecLine("OK\ta2dp-sink"), null);
|
|
// A codec outside the vocabulary stays selectable under its profile name.
|
|
const unknown = context.parseCodecLine("CODEC\t9\ta2dp-sink-brandnew_x\tHigh Fidelity Playback (A2DP Sink, codec BrandNew X)\t0");
|
|
assert.equal(unknown.name, "BRANDNEW_X");
|
|
assert.equal(unknown.codec, "");
|
|
assert.equal(unknown.description, "Unknown codec");
|
|
const bareUnknown = context.parseCodecLine("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 未知编解码器)\t1");
|
|
assert.equal(bareUnknown.name, "Unknown");
|
|
assert.equal(bareUnknown.profile, "a2dp-sink");
|
|
assert.equal(bareUnknown.codec, "");
|
|
assert.equal(bareUnknown.current, true);
|
|
assert.equal(bareUnknown.unknown, true);
|
|
assert.equal(entry.unknown, false);
|
|
const headsetUnknown = context.parseCodecLine("CODEC\t4\theadset-head-unit\tHeadset (HSP/HFP)\t0");
|
|
assert.equal(headsetUnknown.name, "Unknown");
|
|
assert.equal(headsetUnknown.profile, "headset-head-unit");
|
|
});
|
|
|
|
test("an active unknown profile cannot claim a codec name before BlueZ lookup", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "codecLabelFromProfile", "getCodecInfo", "parseCodecLine", "codecCategory"]);
|
|
const process = serviceSource.split(" id: codecListProcess")[1].split(" Process {")[0];
|
|
const handlerSource = process.match(/onRead: (data => \{[\s\S]*?\n \})\n \}\n\n onExited/)?.[1];
|
|
assert.ok(handlerSource, "codecListProcess onRead handler moved");
|
|
const state = { detectedCodec: "", currentIndex: -1, availableCodecs: [] };
|
|
context.root = context;
|
|
context.codecListProcess = state;
|
|
const onRead = vm.runInContext(handlerSource, context);
|
|
onRead("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 未知编解码器)\t1");
|
|
assert.equal(state.detectedCodec, "");
|
|
assert.equal(state.currentIndex, 3);
|
|
assert.equal(state.availableCodecs[0].name, "Unknown");
|
|
assert.equal(state.availableCodecs[0].profile, "a2dp-sink");
|
|
onRead("CODEC\t4\theadset-head-unit\tHeadset (HSP/HFP)\t0");
|
|
assert.equal(state.availableCodecs[1].name, "Unknown");
|
|
assert.notEqual(state.availableCodecs[0].index, state.availableCodecs[1].index);
|
|
const exitSource = process.match(/onExited: (function \(exitCode\) \{[\s\S]*?\n \})\n \}/)?.[1];
|
|
assert.ok(exitSource, "codecListProcess onExited handler moved");
|
|
state.callback = (codecs, current, currentIndex) => {
|
|
assert.equal(codecs.length, 2);
|
|
assert.equal(current, "");
|
|
assert.equal(currentIndex, 3);
|
|
};
|
|
Object.assign(context, state);
|
|
vm.runInContext(`(${exitSource})`, context)(0);
|
|
assert.equal(context.currentIndex, -1);
|
|
});
|
|
|
|
test("an unknown active PipeWire profile preserves choices but defers current codec to BlueZ", () => {
|
|
const context = methods(["refreshDeviceCodec", "getAvailableCodecs", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const pipewire = [{ name: "Unknown", profile: "a2dp-sink", index: 3 }];
|
|
const bluez = [{ name: "LHDC v5" }];
|
|
let bluezQueries = 0;
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = {
|
|
invalidateCodecQuery: context.invalidateCodecQuery,
|
|
codecQueryIsCurrent: context.codecQueryIsCurrent,
|
|
codecQueryVersions: {},
|
|
adapter: { devices: { values: [device] } },
|
|
wpexecAvailable: true,
|
|
dbusBridgeAvailable: true,
|
|
queryCardProfiles: (_device, callback) => callback(pipewire, "", 3),
|
|
queryBluezCodecState: (_device, callback) => { bluezQueries++; callback(bluez, "LHDC v5"); },
|
|
updateDeviceCodec: (_address, name) => updates.push(name)
|
|
};
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5"]);
|
|
context.getAvailableCodecs(device, (codecs, current, currentIndex) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "LHDC v5");
|
|
assert.equal(currentIndex, 3);
|
|
});
|
|
assert.equal(bluezQueries, 2);
|
|
|
|
context.root.queryBluezCodecState = (_device, callback) => callback([], "");
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5", "Unknown"]);
|
|
context.getAvailableCodecs(device, (codecs, current, currentIndex) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
assert.equal(currentIndex, 3);
|
|
});
|
|
context.root.queryBluezCodecState = (_device, callback) => callback([], "VENDOR");
|
|
context.refreshDeviceCodec(device);
|
|
assert.equal(updates.at(-1), "Unknown");
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
});
|
|
context.root.dbusBridgeAvailable = false;
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
});
|
|
});
|
|
|
|
test("a late BlueZ lookup cannot undo a successful codec switch", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "updateDeviceCodec", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const updates = [];
|
|
let reply;
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.deviceCodecs = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
assert.equal(typeof reply, "function");
|
|
// The selector invalidates its pending lookup when switching starts.
|
|
context.invalidateCodecQuery(device.address);
|
|
context.updateDeviceCodec(device.address, "AAC");
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, ["AAC"]);
|
|
});
|
|
|
|
test("only the newest refresh for a device can publish a codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const other = { connected: true, address: "CC:DD" };
|
|
const replies = new Map();
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device, other] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (target, callback) => {
|
|
const callbacks = replies.get(target.address) || [];
|
|
callbacks.push(callback);
|
|
replies.set(target.address, callbacks);
|
|
};
|
|
context.updateDeviceCodec = (address, name) => updates.push([address, name]);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
context.refreshDeviceCodec(other);
|
|
context.refreshDeviceCodec(device);
|
|
replies.get(device.address)[0]([], "LDAC");
|
|
replies.get(other.address)[0]([], "SBC");
|
|
replies.get(device.address)[1]([], "AAC");
|
|
assert.deepEqual(updates, [["CC:DD", "SBC"], ["AA:BB", "AAC"]]);
|
|
});
|
|
|
|
test("a replaced adapter cannot receive a late BlueZ codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const updates = [];
|
|
let reply;
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { dbusPath: "/adapter/old", devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
context.adapter = { dbusPath: "/adapter/new", devices: { values: [device] } };
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, []);
|
|
});
|
|
|
|
test("a disconnected device cannot receive a late BlueZ codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
let reply;
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
device.connected = false;
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, []);
|
|
});
|
|
|
|
test("known PipeWire codec does not query BlueZ again", () => {
|
|
const context = methods(["refreshDeviceCodec", "getAvailableCodecs", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const pipewire = [{ name: "LHDC v5", index: 3 }];
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = {
|
|
invalidateCodecQuery: context.invalidateCodecQuery,
|
|
codecQueryIsCurrent: context.codecQueryIsCurrent,
|
|
codecQueryVersions: {},
|
|
adapter: { devices: { values: [device] } },
|
|
wpexecAvailable: true, dbusBridgeAvailable: true,
|
|
queryCardProfiles: (_device, callback) => callback(pipewire, "LHDC v5", 3),
|
|
queryBluezCodecState: () => assert.fail("known current codec should not query BlueZ"),
|
|
updateDeviceCodec: (_address, name) => updates.push(name)
|
|
};
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5"]);
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "LHDC v5");
|
|
});
|
|
});
|
|
|
|
test("vendor codecs are read from the BlueZ configuration bytes", () => {
|
|
const context = methods(["bytesFromDbusValue", "codecNameFromBluez"]);
|
|
const nameOf = (vendor, codec) => context.codecNameFromBluez(0xff, bluezConfiguration(vendor, codec));
|
|
assert.equal(nameOf(0x0000004f, 0x0001), "APTX");
|
|
assert.equal(nameOf(0x000000d7, 0x0024), "APTX_HD");
|
|
assert.equal(nameOf(0x0000012d, 0x00aa), "LDAC");
|
|
assert.equal(nameOf(0x0000053a, 0x4c35), "LHDC_V5");
|
|
assert.equal(nameOf(0x0000053a, 0x4c33), "LHDC_V3");
|
|
assert.equal(nameOf(0x0000ffff, 0x1234), "VENDOR");
|
|
assert.equal(context.codecNameFromBluez(0x00, []), "SBC");
|
|
assert.equal(context.codecNameFromBluez(0x02, []), "AAC");
|
|
assert.equal(context.codecNameFromBluez(0x04, []), "ATRAC");
|
|
assert.equal(context.codecNameFromBluez(0x03, []), "");
|
|
assert.equal(context.codecNameFromBluez(0xff, [0x3a, 0x05]), "VENDOR");
|
|
assert.equal(context.codecNameFromBluez(0xff, []), "VENDOR");
|
|
});
|
|
|
|
test("LHDC codecs carry a name, description and category", () => {
|
|
const context = methods(["codecMap", "getCodecInfo", "codecCategory", "codecNameFromProfile"]);
|
|
const v5 = context.getCodecInfo("LHDC_V5");
|
|
assert.equal(v5.name, "LHDC v5");
|
|
assert.notEqual(v5.description, "Unknown codec");
|
|
assert.equal(context.getCodecInfo("lhdc-v5").name, "LHDC v5");
|
|
assert.equal(context.getCodecInfo("LHDC_V3").name, "LHDC v3");
|
|
assert.equal(context.codecCategory(v5.name, "a2dp-sink"), "media");
|
|
assert.equal(context.codecCategory(v5.name, "headset-head-unit"), "call");
|
|
assert.equal(context.codecNameFromProfile("a2dp-sink"), "");
|
|
assert.equal(context.codecNameFromProfile("a2dp-sink-aac"), "AAC");
|
|
});
|