[IMP] mail: add ptt extension for chromium-based browsers
Currently, discuss push-to-talk only works when the browser has focus because key events are not fired otherwise. This commit adds an extension for chromium-based browsers that allows to register a system-wide push-to-talk shortcut and adapts the rtc service in order to listen to the extension events. closes odoo/odoo#140752 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Discuss Push-to-Talk Extension
|
||||
|
||||
This extension enhances your communication experience by providing system-wide
|
||||
Push-to-Talk functionality. Say goodbye to tab-specific limitations, as this
|
||||
extension allows you to effortlessly utilize Push-to-Talk across your entire
|
||||
system. Whether Chrome is in focus or not, on the same tab or not, enjoy
|
||||
seamless communication in Odoo Discuss without any constraints.
|
||||
|
||||
> Note that this extension only works for Odoo 17 and higher
|
||||
|
||||
## Installing the extension
|
||||
|
||||
- Go to the chrome extension panel: `chrome://extensions/`.
|
||||
- Click on `Load unpacked`.
|
||||
- Select the `push-to-talk-extension` folder.
|
||||
- Your extension is active!
|
||||
|
||||
## Extension State
|
||||
|
||||
You can check whether your voice transmission is activated at anytime by looking
|
||||
at the extension icon.
|
||||
- <img src="assets/icons/active_icon.png" alt="active_icon" width="24"/> Indicates the voice transmission is active.
|
||||
- <img src="assets/icons/inactive_icon.png" alt="inactive_icon" width="24"/>
|
||||
Indicates the voice transmission is inactive.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Before installing the Discuss Push-to-Talk Extension, ensure that the `Repeat
|
||||
key` feature is enabled on your system.
|
||||
- Command scope must be set to `global`. You can click the extension icon to get
|
||||
to `Keyboard shortcuts` settings or access `chrome://extensions/shortcuts`
|
||||
directly.
|
||||
- Push-to-Talk should be enabled on Odoo Discuss voice settings.
|
||||
|
||||
## Commands
|
||||
|
||||
- `Push-to-Talk`: Voice is activated as long as the key is held.
|
||||
- `Voice Toggle`: Switch your voice transmission on or off.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,87 @@
|
||||
/* global chrome */
|
||||
|
||||
import { throttle } from "./utils.js";
|
||||
|
||||
const ACTIVE_APP_ICON = "/assets/icons/active_icon.png";
|
||||
const INACTIVE_APP_ICON = "/assets/icons/inactive_icon.png";
|
||||
|
||||
// ServiceWorker can be terminated at any time, storage session is
|
||||
// used to ensure local state is not lost.
|
||||
chrome.storage.session.set({ isTalkingByTabId: {} });
|
||||
|
||||
chrome.tabs.onRemoved.addListener(async (tabId) => {
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
delete isTalkingByTabId[tabId];
|
||||
await chrome.storage.session.set({ isTalkingByTabId });
|
||||
await updateAppIcon();
|
||||
});
|
||||
|
||||
chrome.action.onClicked.addListener(function () {
|
||||
chrome.tabs.create({ url: "chrome://extensions/shortcuts" });
|
||||
});
|
||||
|
||||
async function updateAppIcon() {
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
const isTalking = Object.values(isTalkingByTabId).some(Boolean);
|
||||
chrome.action.setIcon({ path: isTalking ? ACTIVE_APP_ICON : INACTIVE_APP_ICON });
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(async function (request, sender) {
|
||||
const { from, type, value } = request;
|
||||
if (from !== "discuss") {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case "subscribe":
|
||||
{
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
isTalkingByTabId[sender.tab.id] = false;
|
||||
await chrome.storage.session.set({ isTalkingByTabId });
|
||||
}
|
||||
break;
|
||||
case "unsubscribe":
|
||||
{
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
delete isTalkingByTabId[sender.tab.id];
|
||||
await chrome.storage.session.set({ isTalkingByTabId });
|
||||
await updateAppIcon();
|
||||
}
|
||||
break;
|
||||
case "is-talking":
|
||||
{
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
isTalkingByTabId[sender.tab.id] = value;
|
||||
await chrome.storage.session.set({ isTalkingByTabId });
|
||||
await updateAppIcon();
|
||||
}
|
||||
break;
|
||||
case "ask-is-enabled":
|
||||
chrome.tabs.sendMessage(sender.tab.id, {
|
||||
from: "discuss-push-to-talk",
|
||||
type: "answer-is-enabled",
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
async function onCommand(command) {
|
||||
const { isTalkingByTabId } = await chrome.storage.session.get();
|
||||
for (const tabId of Object.keys(isTalkingByTabId)) {
|
||||
switch (command) {
|
||||
case "toggle-voice":
|
||||
chrome.tabs.sendMessage(Number(tabId), {
|
||||
from: "discuss-push-to-talk",
|
||||
type: "toggle-voice",
|
||||
});
|
||||
break;
|
||||
case "ptt-pressed":
|
||||
chrome.tabs.sendMessage(Number(tabId), {
|
||||
from: "discuss-push-to-talk",
|
||||
type: "push-to-talk-pressed",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chrome.commands.onCommand.addListener(throttle(onCommand, 150));
|
||||
@@ -0,0 +1,11 @@
|
||||
/* global chrome */
|
||||
|
||||
window.addEventListener("message", function ({ data }) {
|
||||
if (data.from === "discuss") {
|
||||
chrome.runtime.sendMessage(data);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request) {
|
||||
window.postMessage(request);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Discuss Push-to-talk",
|
||||
"description": "Empowers Odoo Discuss with system-wide Push-to-Talk",
|
||||
"version": "1.0.0.0",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"action": {},
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"icons": {
|
||||
"128": "assets/icons/inactive_icon.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://*/*",
|
||||
"http://*/*"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"ptt-pressed": {
|
||||
"suggested_key": "Alt+Shift+Y",
|
||||
"description": "Push-to-Talk"
|
||||
},
|
||||
"toggle-voice": {
|
||||
"suggested_key": "Alt+Shift+U",
|
||||
"description": "Voice Toggle"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function throttle(fn, delay) {
|
||||
let timeout = null;
|
||||
|
||||
return function (...args) {
|
||||
if (timeout === null) {
|
||||
fn(...args);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export const pttExtensionHookService = {
|
||||
start(env) {
|
||||
const INITIAL_RELEASE_TIMEOUT = 500;
|
||||
const COMMON_RELEASE_TIMEOUT = 200;
|
||||
let isEnabled = false;
|
||||
let voiceActivated = false;
|
||||
|
||||
browser.addEventListener("message", ({ data }) => {
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
if (data.from !== "discuss-push-to-talk" || !rtc) {
|
||||
return;
|
||||
}
|
||||
switch (data.type) {
|
||||
case "push-to-talk-pressed":
|
||||
{
|
||||
voiceActivated = false;
|
||||
const isFirstPress = !rtc.state.selfSession?.isTalking;
|
||||
rtc.onPushToTalk();
|
||||
if (rtc.state.selfSession?.isTalking) {
|
||||
// Second key press is slow to come thus, the first timeout
|
||||
// must be greater than the following ones.
|
||||
rtc.setPttReleaseTimeout(
|
||||
isFirstPress ? INITIAL_RELEASE_TIMEOUT : COMMON_RELEASE_TIMEOUT
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "toggle-voice":
|
||||
{
|
||||
if (voiceActivated) {
|
||||
rtc.setPttReleaseTimeout(0);
|
||||
} else {
|
||||
rtc.onPushToTalk();
|
||||
}
|
||||
voiceActivated = !voiceActivated;
|
||||
}
|
||||
break;
|
||||
case "answer-is-enabled":
|
||||
isEnabled = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
window.postMessage({ from: "discuss", type: "ask-is-enabled" });
|
||||
|
||||
return {
|
||||
notifyIsTalking(isTalking) {
|
||||
window.postMessage({ from: "discuss", type: "is-talking", value: isTalking });
|
||||
},
|
||||
subscribe() {
|
||||
window.postMessage({ from: "discuss", type: "subscribe" });
|
||||
},
|
||||
unsubscribe() {
|
||||
voiceActivated = false;
|
||||
window.postMessage({ from: "discuss", type: "unsubscribe" });
|
||||
},
|
||||
get isEnabled() {
|
||||
return isEnabled;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("discuss.ptt_extension", pttExtensionHookService);
|
||||
@@ -99,6 +99,7 @@ export class Rtc {
|
||||
this.rpc = services.rpc;
|
||||
this.soundEffectsService = services["mail.sound_effects"];
|
||||
this.userSettingsService = services["mail.user_settings"];
|
||||
this.pttExtService = services["discuss.ptt_extension"];
|
||||
this.state = reactive({
|
||||
hasPendingRequest: false,
|
||||
selfSession: undefined,
|
||||
@@ -162,19 +163,10 @@ export class Rtc {
|
||||
browser.addEventListener(
|
||||
"keydown",
|
||||
(ev) => {
|
||||
if (
|
||||
!this.state.channel ||
|
||||
this.userSettingsService.isRegisteringKey ||
|
||||
!this.userSettingsService.usePushToTalk ||
|
||||
!this.userSettingsService.isPushToTalkKey(ev)
|
||||
) {
|
||||
if (!this.userSettingsService.isPushToTalkKey(ev)) {
|
||||
return;
|
||||
}
|
||||
browser.clearTimeout(this.state.pttReleaseTimeout);
|
||||
if (!this.state.selfSession.isTalking && !this.state.selfSession.isMute) {
|
||||
this.soundEffectsService.play("push-to-talk-on", { volume: 0.3 });
|
||||
}
|
||||
this.setTalking(true);
|
||||
this.onPushToTalk();
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
@@ -189,12 +181,7 @@ export class Rtc {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.state.pttReleaseTimeout = browser.setTimeout(() => {
|
||||
this.setTalking(false);
|
||||
if (!this.state.selfSession?.isMute) {
|
||||
this.soundEffectsService.play("push-to-talk-off", { volume: 0.3 });
|
||||
}
|
||||
}, Math.max(this.userSettingsService.voiceActiveDuration || 0, 200));
|
||||
this.setPttReleaseTimeout();
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
@@ -231,6 +218,29 @@ export class Rtc {
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
setPttReleaseTimeout(duration = 200) {
|
||||
this.state.pttReleaseTimeout = browser.setTimeout(() => {
|
||||
this.setTalking(false);
|
||||
if (!this.state.selfSession?.isMute) {
|
||||
this.soundEffectsService.play("push-to-talk-off", { volume: 0.3 });
|
||||
}
|
||||
}, Math.max(this.userSettingsService.voiceActiveDuration || 0, duration));
|
||||
}
|
||||
|
||||
onPushToTalk() {
|
||||
if (
|
||||
!this.state.channel ||
|
||||
this.userSettingsService.isRegisteringKey ||
|
||||
!this.userSettingsService.usePushToTalk
|
||||
) {
|
||||
return;
|
||||
}
|
||||
browser.clearTimeout(this.state.pttReleaseTimeout);
|
||||
if (!this.state.selfSession.isTalking && !this.state.selfSession.isMute) {
|
||||
this.soundEffectsService.play("push-to-talk-on", { volume: 0.3 });
|
||||
}
|
||||
this.setTalking(true);
|
||||
}
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {any} param0.id
|
||||
@@ -275,6 +285,7 @@ export class Rtc {
|
||||
channel.rtcInvitingSession = undefined;
|
||||
channel.activeRtcSession = undefined;
|
||||
if (channel.eq(this.state.channel)) {
|
||||
this.pttExtService.unsubscribe();
|
||||
this.clear();
|
||||
this.soundEffectsService.play("channel-leave");
|
||||
}
|
||||
@@ -718,6 +729,7 @@ export class Rtc {
|
||||
this.notification.add(_t("Your browser does not support webRTC."), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
this.pttExtService.subscribe();
|
||||
const { rtcSessions, iceServers, sessionId } = await this.rpc(
|
||||
"/mail/rtc/channel/join_call",
|
||||
{
|
||||
@@ -1067,6 +1079,7 @@ export class Rtc {
|
||||
}
|
||||
this.state.selfSession.isTalking = isTalking;
|
||||
if (!this.state.selfSession.isMute) {
|
||||
this.pttExtService.notifyIsTalking(isTalking);
|
||||
await this.refreshAudioStatus();
|
||||
}
|
||||
}
|
||||
@@ -1552,6 +1565,7 @@ export class Rtc {
|
||||
export const rtcService = {
|
||||
dependencies: [
|
||||
"bus_service",
|
||||
"discuss.ptt_extension",
|
||||
"mail.sound_effects",
|
||||
"mail.store",
|
||||
"mail.user_settings",
|
||||
|
||||
Reference in New Issue
Block a user