[FIX] mail: call do not keep the camera stream when switching to display
Before this commit, trying to share screen while using the user video (webcam) wouldn't work because the old stream was kept instead of trying to obtain a new one. This commit fixes this issue, does a small cleanup of `rtc_service.js` and adds tests related to streams. closes odoo/odoo#116042 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -136,7 +136,7 @@ export class Rtc {
|
||||
pttReleaseTimeout: undefined,
|
||||
sourceCameraStream: null,
|
||||
});
|
||||
this.blur = undefined;
|
||||
this.blurManager = undefined;
|
||||
this.ringingThreads = reactive([], () => this.onRingingThreadsChange());
|
||||
void this.ringingThreads.length;
|
||||
this.store.ringingThreads = this.ringingThreads;
|
||||
@@ -1110,7 +1110,7 @@ export class Rtc {
|
||||
let sourceStream;
|
||||
try {
|
||||
if (type === "camera") {
|
||||
if (this.state.sourceCameraStream) {
|
||||
if (this.state.sourceCameraStream && this.state.sendCamera) {
|
||||
sourceStream = this.state.sourceCameraStream;
|
||||
} else {
|
||||
sourceStream = await browser.navigator.mediaDevices.getUserMedia({
|
||||
@@ -1190,22 +1190,13 @@ export class Rtc {
|
||||
} else {
|
||||
transceiver = getTransceiver(session.peerConnection, trackKind);
|
||||
}
|
||||
if (track) {
|
||||
try {
|
||||
await transceiver.sender.replaceTrack(track);
|
||||
transceiver.direction = transceiverDirection;
|
||||
} catch {
|
||||
// ignored, the track is probably already on the peerConnection.
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await transceiver.sender.replaceTrack(null);
|
||||
await transceiver.sender.replaceTrack(track || null);
|
||||
transceiver.direction = transceiverDirection;
|
||||
} catch {
|
||||
// ignored, the transceiver is probably already removed
|
||||
this.log(session, `failed to update ${trackKind} transceiver`);
|
||||
}
|
||||
if (trackKind === "video") {
|
||||
if (!track && trackKind === "video") {
|
||||
this.notify([session], "trackChange", {
|
||||
type: "video",
|
||||
state: { isSendingVideo: false },
|
||||
@@ -1218,7 +1209,7 @@ export class Rtc {
|
||||
this.state.audioTrack.stop();
|
||||
this.state.audioTrack = undefined;
|
||||
}
|
||||
if (!this.state.channel.id) {
|
||||
if (!this.state.channel) {
|
||||
return;
|
||||
}
|
||||
if (force) {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { afterNextRender, click, start, startServer } from "@mail/../tests/helpers/test_utils";
|
||||
import {
|
||||
afterNextRender,
|
||||
click,
|
||||
start,
|
||||
startServer,
|
||||
mockGetMedia,
|
||||
} from "@mail/../tests/helpers/test_utils";
|
||||
import { editInput, nextTick, patchWithCleanup, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
QUnit.module("call");
|
||||
|
||||
QUnit.test("basic rendering", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["mail.channel"].create({
|
||||
name: "General",
|
||||
@@ -54,9 +61,10 @@ QUnit.test("should not display call UI when no more members (self disconnect)",
|
||||
});
|
||||
|
||||
QUnit.test("show call UI in chat window when in call", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["mail.channel"].create({ name: "General" });
|
||||
const { env } = await start();
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem:contains(General)");
|
||||
assert.containsOnce($, ".o-mail-ChatWindow");
|
||||
@@ -72,17 +80,13 @@ QUnit.test("show call UI in chat window when in call", async (assert) => {
|
||||
$,
|
||||
".o-mail-ChatWindow-header .o-mail-ChatWindow-command[title='Start a Call']"
|
||||
);
|
||||
/**
|
||||
* during the tests, the browser is not really closed,
|
||||
* so we need to end the call manually to avoid memory leaks.
|
||||
*/
|
||||
env.services["mail.rtc"]?.endCall();
|
||||
});
|
||||
|
||||
QUnit.test("should disconnect when closing page while in call", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["mail.channel"].create({ name: "General" });
|
||||
const { openDiscuss, env } = await start();
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
@@ -104,11 +108,6 @@ QUnit.test("should disconnect when closing page while in call", async (assert) =
|
||||
await afterNextRender(() => window.dispatchEvent(new Event("pagehide"), { bubble: true }));
|
||||
await nextTick();
|
||||
assert.verifySteps(["sendBeacon_leave_call"]);
|
||||
/**
|
||||
* during the tests, the browser is not really closed,
|
||||
* so we need to end the call manually to avoid memory leaks.
|
||||
*/
|
||||
env.services["mail.rtc"]?.endCall();
|
||||
});
|
||||
|
||||
QUnit.test("no default rtc after joining a chat conversation", async (assert) => {
|
||||
@@ -205,3 +204,33 @@ QUnit.test("should display invitations", async (assert) => {
|
||||
assert.containsNone($, ".o-mail-CallInvitation");
|
||||
assert.verifySteps(["pause_sound_effect"]);
|
||||
});
|
||||
|
||||
QUnit.test("can share screen", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["mail.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Discuss-header button[title='Start a Call']");
|
||||
await click(".o-mail-CallActionList button[title='Share screen']");
|
||||
assert.containsOnce($, ".o-mail-CallParticipantCard video");
|
||||
await click(".o-mail-CallActionList button[title='Stop screen sharing']");
|
||||
assert.containsNone($, ".o-mail-CallParticipantCard video");
|
||||
});
|
||||
|
||||
QUnit.test("can share user camera", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["mail.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Discuss-header button[title='Start a Call']");
|
||||
await click(".o-mail-CallActionList button[title='Turn camera on']");
|
||||
assert.containsOnce($, ".o-mail-CallParticipantCard video");
|
||||
await click(".o-mail-CallActionList button[title='Stop camera']");
|
||||
assert.containsNone($, ".o-mail-CallParticipantCard video");
|
||||
});
|
||||
|
||||
@@ -583,6 +583,77 @@ function isScrolledTo(el, scrollTop) {
|
||||
return Math.abs(el.scrollTop - scrollTop) <= 1;
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: web API utilities
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Mocks the browser's `navigator.mediaDevices.getUserMedia` and `navigator.mediaDevices.getDisplayMedia`
|
||||
*/
|
||||
export function mockGetMedia() {
|
||||
class MockMediaStreamTrack extends EventTarget {
|
||||
enabled = true;
|
||||
readyState = "live";
|
||||
constructor(kind) {
|
||||
super();
|
||||
this.kind = kind;
|
||||
}
|
||||
stop() {
|
||||
this.readyState = "ended";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The audio streams are mocked as there is no way to create a MediaStream
|
||||
* with an audio track without really requesting it from the device.
|
||||
*/
|
||||
class MockAudioMediaStream extends MediaStream {
|
||||
mockTracks = [new MockMediaStreamTrack("audio")];
|
||||
getTracks() {
|
||||
return this.mockTracks;
|
||||
}
|
||||
getAudioTracks() {
|
||||
return this.mockTracks;
|
||||
}
|
||||
getVideoTracks() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const streams = [];
|
||||
/**
|
||||
* The video streams are real MediaStreams created from a 1x1 canvas at 1fps.
|
||||
*/
|
||||
const createVideoStream = (constraints) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const stream = canvas.captureStream(1);
|
||||
return stream;
|
||||
};
|
||||
patchWithCleanup(browser.navigator.mediaDevices, {
|
||||
getUserMedia(constraints) {
|
||||
let stream;
|
||||
if (constraints.audio) {
|
||||
stream = new MockAudioMediaStream();
|
||||
} else {
|
||||
// The video streams are real MediaStreams
|
||||
stream = createVideoStream();
|
||||
}
|
||||
streams.push(stream);
|
||||
return stream;
|
||||
},
|
||||
getDisplayMedia() {
|
||||
const stream = createVideoStream();
|
||||
streams.push(stream);
|
||||
return stream;
|
||||
},
|
||||
});
|
||||
registerCleanup(() => {
|
||||
// stop all streams as some tests may not do actions that lead to the ending of tracks
|
||||
streams.forEach((stream) => {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
});
|
||||
});
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Export
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user