Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mail_livekit/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
"mail_livekit/static/src/discuss/call_participant_video_patch.js",
"mail_livekit/static/src/discuss/call_context_menu_patch.js",
],
"web.assets_unit_tests": [
"mail_livekit/static/lib/livekit/livekit-client.umd.min.js",
"mail_livekit/static/src/discuss/livekit_service.js",
"mail_livekit/static/src/discuss/livekit_adapter.js",
"mail_livekit/static/tests/**/*",
],
},
"external_dependencies": {
"python": ["livekit-api"],
Expand Down
26 changes: 17 additions & 9 deletions mail_livekit/static/src/discuss/livekit_adapter.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @odoo-module */

import {Source, livekitService} from "./livekit_service";

export class LiveKitAdapter {
Expand Down Expand Up @@ -29,12 +31,13 @@ export class LiveKitAdapter {
}

async updateUpload(source, track) {
const livekitSource =
source === "audio"
? Source.MICROPHONE
: source === "camera"
? Source.CAMERA
: Source.SCREEN;
const livekitSource = Object.values(Source).includes(source)
? source
: source === "audio"
? Source.MICROPHONE
: source === "camera"
? Source.CAMERA
: Source.SCREEN;
await livekitService?.setTrackEnabled(livekitSource, Boolean(track), track);
}

Expand Down Expand Up @@ -87,14 +90,14 @@ export class LiveKitAdapter {
});
livekitService.subscribeToTrackSubscribed(
"adapter",
(participantId, source, track) => {
(participantId, source, track, audioElement = null) => {
console.debug(
"received Track subscribed event:",
participantId,
source,
track
);
this.handleTrackSubscribed(participantId, source, track);
this.handleTrackSubscribed(participantId, source, track, audioElement);
}
);
livekitService.subscribeToTrackMuted(
Expand All @@ -113,8 +116,13 @@ export class LiveKitAdapter {
}

async connect(livekit_url, token) {
await livekitService?.connect(livekit_url, token);
this.addLivekitListeners();
try {
await livekitService?.connect(livekit_url, token);
} catch (error) {
await livekitService?.disconnect();
throw error;
}
}

async disconnect() {
Expand Down
6 changes: 4 additions & 2 deletions mail_livekit/static/src/discuss/livekit_service.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @odoo-module */

const {Room, VideoPresets, RoomEvent} = window.LivekitClient;

// This script should be the only contact point with Livekit SDK
Expand Down Expand Up @@ -151,8 +153,8 @@ class LivekitService {

// Add this method to the LivekitService class
async rebindExistingTracks() {
if (!this.room || !this.connected) {
log("Cannot rebind - not connected to room");
if (!this.room) {
log("Cannot rebind - no LiveKit room");
return;
}

Expand Down
110 changes: 110 additions & 0 deletions mail_livekit/static/tests/livekit_adapter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {after, afterEach, describe, expect, test} from "@odoo/hoot";
import {Source, livekitService} from "@mail_livekit/discuss/livekit_service";
import {LiveKitAdapter} from "@mail_livekit/discuss/livekit_adapter";

const originalLivekitClient = window.LivekitClient;

function cleanupLivekitService() {
livekitService.infoChangeListeners.clear();
livekitService.trackSubscribedListeners.clear();
livekitService.trackMutedListeners.clear();
livekitService.room = null;
livekitService.connected = false;
livekitService.initiated = false;
document
.querySelectorAll(`.${livekitService.audioElementClass}`)
.forEach((element) => element.remove());
}

function makeRemoteAudioTrack(identity) {
const audioElement = document.createElement("audio");
const track = {
kind: "audio",
attach: () => audioElement,
detach: () => {
// Detach the audio element
},
};
const publication = {
source: Source.MICROPHONE,
track,
isSubscribed: true,
};
const participant = {
identity,
sid: `${identity}-sid`,
audioTrackPublications: new Map([["microphone", publication]]),
videoTrackPublications: new Map(),
};
return {audioElement, participant, publication, track};
}

describe("mail_livekit livekit adapter", () => {
after(() => {
window.LivekitClient = originalLivekitClient;
});

afterEach(() => {
cleanupLivekitService();
});

test("connect does not drop audio subscribed while the room connection is starting", async () => {
const adapter = new LiveKitAdapter();
const emittedPayloads = [];
const {audioElement, participant, publication, track} =
makeRemoteAudioTrack("partner:8");
const originalConnect = livekitService.connect;

livekitService.connect = async () => {
livekitService.handleTrackSubscribed(track, publication, participant);
};

adapter.addEventListener("setAudioVolume", (event) => {
emittedPayloads.push(event.detail.payload);
});

try {
await adapter.connect("wss://livekit.example", "token");
} finally {
livekitService.connect = originalConnect;
}

expect(emittedPayloads).toEqual([
{
element: audioElement,
identity: "partner:8",
},
]);
});

test("rebindExistingTracks replays audio already subscribed during connection startup", async () => {
const {audioElement, participant, track} = makeRemoteAudioTrack("partner:9");
const subscribedTracks = [];

livekitService.room = {
remoteParticipants: new Map([[participant.identity, participant]]),
};
livekitService.connected = false;
livekitService.subscribeToTrackSubscribed(
"adapter",
(identity, source, subscribedTrack, element) => {
subscribedTracks.push({
element,
identity,
source,
track: subscribedTrack,
});
}
);

await livekitService.rebindExistingTracks();

expect(subscribedTracks.length).toBe(1);
expect(subscribedTracks[0]).toEqual({
element: audioElement,
identity: "partner:9",
source: Source.MICROPHONE,
track,
});
});
});
Loading