fix(lotus): soundboard says "not permitted" in audio-only rooms instead of failing silently (#47)
Clips publish as an Unknown-source track. In audio-only rooms the guard
signs a microphone-only token, LiveKit refuses the publish, and since
the fork had already replied {} the host never heard. Check the grants
up front and reply { played: false, reason: "not_permitted" }.
Refs #47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
58625c86b0
commit
888ed1a66c
@@ -11,6 +11,7 @@ import { of } from "rxjs";
|
|||||||
|
|
||||||
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||||
import {
|
import {
|
||||||
|
canPublishSoundboard,
|
||||||
MAX_INJECT_BYTES,
|
MAX_INJECT_BYTES,
|
||||||
parseInjectSource,
|
parseInjectSource,
|
||||||
startLotusAudioInject,
|
startLotusAudioInject,
|
||||||
@@ -258,3 +259,17 @@ test("parseInjectSource rejects oversized or non-buffer audio", () => {
|
|||||||
expect(parseInjectSource({ audio: "not bytes" })).toBeNull();
|
expect(parseInjectSource({ audio: "not bytes" })).toBeNull();
|
||||||
expect(parseInjectSource({ audio: new Uint8Array(4) })).toBeNull();
|
expect(parseInjectSource({ audio: new Uint8Array(4) })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("canPublishSoundboard: refused by a microphone-only grant (EC #47)", () => {
|
||||||
|
expect(canPublishSoundboard(undefined)).toBe(true);
|
||||||
|
expect(
|
||||||
|
canPublishSoundboard({ canPublish: true, canPublishSources: [] }),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
canPublishSoundboard({ canPublish: true, canPublishSources: [2] }),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
canPublishSoundboard({ canPublish: true, canPublishSources: [2, 0] }),
|
||||||
|
).toBe(true);
|
||||||
|
expect(canPublishSoundboard({ canPublish: false })).toBe(false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -60,6 +60,22 @@ function acquireSharedAudio(): {
|
|||||||
* No effect unless the host sends the action. Returns a teardown function that
|
* No effect unless the host sends the action. Returns a teardown function that
|
||||||
* also aborts any clip still playing.
|
* also aborts any clip still playing.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Pure: whether these LiveKit permissions allow publishing the soundboard's
|
||||||
|
* Unknown-source track (EC #47). No permissions or an empty source list
|
||||||
|
* means unrestricted; otherwise UNKNOWN (TrackSource 0) must be allowed.
|
||||||
|
*/
|
||||||
|
export function canPublishSoundboard(
|
||||||
|
perm:
|
||||||
|
| { canPublish?: boolean; canPublishSources?: readonly number[] }
|
||||||
|
| undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!perm) return true;
|
||||||
|
if (perm.canPublish === false) return false;
|
||||||
|
const sources = perm.canPublishSources ?? [];
|
||||||
|
return sources.length === 0 || sources.includes(0);
|
||||||
|
}
|
||||||
|
|
||||||
export function startLotusAudioInject(vm: CallViewModel): () => void {
|
export function startLotusAudioInject(vm: CallViewModel): () => void {
|
||||||
const w = widget;
|
const w = widget;
|
||||||
if (!w) return () => undefined;
|
if (!w) return () => undefined;
|
||||||
@@ -116,6 +132,19 @@ export function startLotusAudioInject(vm: CallViewModel): () => void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [EC #47] Clips publish as an Unknown-source track. Audio-only rooms get
|
||||||
|
// a token limited to the microphone source, where LiveKit refuses that
|
||||||
|
// publish *after* we replied; say so up front instead of failing silently.
|
||||||
|
if (
|
||||||
|
rooms.some((r) => !canPublishSoundboard(r.localParticipant.permissions))
|
||||||
|
) {
|
||||||
|
w.api.transport.reply(ev.detail, {
|
||||||
|
played: false,
|
||||||
|
reason: "not_permitted",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
w.api.transport.reply(ev.detail, {});
|
w.api.transport.reply(ev.detail, {});
|
||||||
void playInjectedClip(clip, volume, rooms, activeClips).catch((e) =>
|
void playInjectedClip(clip, volume, rooms, activeClips).catch((e) =>
|
||||||
logger.warn("[lotus] inject_audio failed", e),
|
logger.warn("[lotus] inject_audio failed", e),
|
||||||
|
|||||||
Reference in New Issue
Block a user