[ADD] discuss: add sfu server support for calls
The SFU is a server used to relay streaming between the participants of a call to provide better scalability and performance than the current peer-to-peer connections. The required fields have been added in https://github.com/odoo/odoo/pull/138581 * `/mail/static/lib/discuss_sfu` contains the client code that is used by the odoo client to interact with the SFU. From: https://github.com/odoo/sfu/releases/tag/v1.0.0 * The Json Web Token code has been extracted from `web_push` while adding support for the HS256 algorithm, to be used as the authentication method between the SFU and Odoo. * The python code has been updated so that the odoo server requests a channel from the SFU when going above a participant threshold, and swap the participants to a SFU connection afterward. * `rtc_service.js` has been updated to support both peer-to-peer and SFU modes. * The `call_context_menu` has been updated to display information relative to the SFU connection when in SFU mode. task-2765922 closes odoo/odoo#132153 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -132,6 +132,7 @@ For more specific needs, you may also assign custom-defined actions
|
||||
'mail/static/src/scss/variables/derived_variables.scss',
|
||||
'mail/static/src/scss/*.scss',
|
||||
'mail/static/lib/**/*',
|
||||
('remove', 'mail/static/lib/odoo_sfu/odoo_sfu.js'),
|
||||
('remove', 'mail/static/lib/lame/lame.js'),
|
||||
'mail/static/src/js/**/*',
|
||||
'mail/static/src/core/common/**/*',
|
||||
@@ -178,6 +179,9 @@ For more specific needs, you may also assign custom-defined actions
|
||||
('remove', 'mail/static/tests/tours/**/*'),
|
||||
('remove', 'mail/static/tests/helpers/**/*'),
|
||||
],
|
||||
'mail.assets_odoo_sfu': [
|
||||
'mail/static/lib/odoo_sfu/odoo_sfu.js',
|
||||
],
|
||||
'mail.assets_lamejs': [
|
||||
'mail/static/lib/lame/lame.js',
|
||||
],
|
||||
|
||||
+149
-230
@@ -74,7 +74,6 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.js:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.js:0
|
||||
#, python-format
|
||||
msgid "%(candidateType)s (%(protocol)s)"
|
||||
msgstr ""
|
||||
@@ -211,7 +210,7 @@ msgstr ""
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/rtc_service.js:0
|
||||
#, python-format
|
||||
msgid "%s raised a hand"
|
||||
msgid "%s raised their hand"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
@@ -881,53 +880,9 @@ msgid "Advanced Options"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_journal__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_move__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_payment__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__crm_lead__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_booth__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_event__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_registration__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_sponsor__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_track__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_contract__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_services__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_applicant__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_contract__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_employee__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense_sheet__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave_allocation__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__lunch_supplier__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_mixin__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_type__decoration_type__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mailing_mailing__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_equipment__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_request__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_production__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_unbuild__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__pos_session__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_pricelist__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_product__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_template__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_project__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_task__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_update__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_order__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_requisition__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__repair_order__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner_bank__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__sale_order__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__slide_channel__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_landed_cost__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_lot__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking_batch__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_survey__activity_exception_decoration__warning
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_user_input__activity_exception_decoration__warning
|
||||
msgid "Alert"
|
||||
msgstr ""
|
||||
|
||||
@@ -1818,7 +1773,6 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/web/messaging_menu.js:0
|
||||
#: code:addons/mail/static/src/core/web/messaging_menu.js:0
|
||||
#: model:ir.model.fields,field_description:mail.field_discuss_channel_member__channel_id
|
||||
#: model:ir.model.fields,field_description:mail.field_discuss_channel_rtc_session__channel_id
|
||||
#: model:ir.model.fields.selection,name:mail.selection__discuss_channel__channel_type__channel
|
||||
@@ -1840,6 +1794,13 @@ msgstr ""
|
||||
msgid "Channel Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/rtc_service.js:0
|
||||
#, python-format
|
||||
msgid "Channel full"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/discuss/discuss_channel_member.py:0
|
||||
@@ -1876,7 +1837,6 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/web/messaging_menu.js:0
|
||||
#: code:addons/mail/static/src/core/web/messaging_menu.js:0
|
||||
#: model:ir.model.fields.selection,name:mail.selection__discuss_channel__channel_type__chat
|
||||
#, python-format
|
||||
msgid "Chat"
|
||||
@@ -2105,8 +2065,8 @@ msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#. odoo-javascript
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/mail_template.py:0
|
||||
#: code:addons/mail/static/src/core/common/message_confirm_dialog.js:0
|
||||
#: model_terms:ir.ui.view,arch_db:mail.mail_template_view_form_confirm_delete
|
||||
@@ -2153,6 +2113,21 @@ msgstr ""
|
||||
msgid "Connection test failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/rtc_service.js:0
|
||||
#, python-format
|
||||
msgid "Connection to SFU server closed by the server"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Connection type:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
@@ -2952,6 +2927,13 @@ msgstr ""
|
||||
msgid "Download logs"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Download:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/common/dropzone.xml:0
|
||||
@@ -3327,53 +3309,9 @@ msgstr ""
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/common/notification_model.js:0
|
||||
#: model:ir.model.fields,field_description:mail.field_mail_activity_schedule__error
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_journal__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_move__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_payment__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__crm_lead__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_booth__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_event__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_registration__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_sponsor__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_track__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_contract__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_services__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_applicant__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_contract__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_employee__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense_sheet__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave_allocation__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__lunch_supplier__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_mixin__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_type__decoration_type__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mailing_mailing__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_equipment__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_request__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_production__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_unbuild__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__pos_session__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_pricelist__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_product__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_template__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_project__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_task__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_update__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_order__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_requisition__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__repair_order__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner_bank__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__sale_order__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__slide_channel__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_landed_cost__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_lot__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking_batch__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_survey__activity_exception_decoration__danger
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_user_input__activity_exception_decoration__danger
|
||||
#, python-format
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
@@ -3470,6 +3408,13 @@ msgstr ""
|
||||
msgid "Failed to load gifs..."
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/rtc_service.js:0
|
||||
#, python-format
|
||||
msgid "Failed to load the SFU server, falling back to peer-to-peer"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/mail_render_mixin.py:0
|
||||
@@ -4893,6 +4838,11 @@ msgstr ""
|
||||
msgid "Keep activities marked as done in the activity view"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.res_config_settings_view_form
|
||||
msgid "Key"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/mail_alias.py:0
|
||||
@@ -6433,7 +6383,6 @@ msgstr ""
|
||||
msgid "Old values"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/common/im_status.xml:0
|
||||
@@ -6602,6 +6551,14 @@ msgstr ""
|
||||
msgid "Original message was deleted"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/res_users.py:0
|
||||
#: model:ir.actions.act_window,name:mail.mail_activity_without_access_action
|
||||
#, python-format
|
||||
msgid "Other activities"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_mail__state__outgoing
|
||||
#: model_terms:ir.ui.view,arch_db:mail.view_mail_search
|
||||
@@ -6639,53 +6596,9 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/web/activity_list_popover.xml:0
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_journal__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_move__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_payment__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__crm_lead__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_booth__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_event__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_registration__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_sponsor__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_track__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_contract__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_services__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_applicant__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_contract__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_employee__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense_sheet__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave_allocation__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__lunch_supplier__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity__state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_mixin__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mailing_mailing__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_equipment__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_request__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_production__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_unbuild__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__pos_session__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_pricelist__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_product__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_template__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_project__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_task__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_update__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_order__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_requisition__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__repair_order__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner_bank__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__sale_order__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__slide_channel__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_landed_cost__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_lot__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking_batch__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_survey__activity_state__overdue
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_user_input__activity_state__overdue
|
||||
#, python-format
|
||||
msgid "Overdue"
|
||||
msgstr ""
|
||||
@@ -6713,6 +6626,7 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Packets received:"
|
||||
msgstr ""
|
||||
@@ -6720,6 +6634,7 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Packets sent:"
|
||||
msgstr ""
|
||||
@@ -6911,53 +6826,9 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/web/activity_list_popover.xml:0
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_journal__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_move__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_payment__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__crm_lead__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_booth__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_event__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_registration__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_sponsor__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_track__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_contract__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_services__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_applicant__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_contract__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_employee__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense_sheet__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave_allocation__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__lunch_supplier__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity__state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_mixin__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mailing_mailing__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_equipment__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_request__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_production__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_unbuild__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__pos_session__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_pricelist__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_product__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_template__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_project__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_task__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_update__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_order__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_requisition__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__repair_order__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner_bank__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__sale_order__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__slide_channel__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_landed_cost__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_lot__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking_batch__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_survey__activity_state__planned
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_user_input__activity_state__planned
|
||||
#, python-format
|
||||
msgid "Planned"
|
||||
msgstr ""
|
||||
@@ -7211,6 +7082,13 @@ msgstr ""
|
||||
msgid "RTC Session"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "RTC Session ID:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model:ir.model.fields,field_description:mail.field_discuss_channel_member__rtc_session_ids
|
||||
msgid "RTC Sessions"
|
||||
@@ -7699,6 +7577,11 @@ msgstr ""
|
||||
msgid "SFU Server key"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.res_config_settings_view_form
|
||||
msgid "SFU server"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.view_email_template_search
|
||||
msgid "SMTP Server"
|
||||
@@ -7754,8 +7637,8 @@ msgid "Schedule & Mark as Done"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#. odoo-javascript
|
||||
#. odoo-python
|
||||
#: code:addons/mail/static/src/core/web/activity_service.js:0
|
||||
#: code:addons/mail/static/src/core/web/activity_service.js:0
|
||||
#: code:addons/mail/wizard/mail_activity_schedule.py:0
|
||||
@@ -7764,8 +7647,8 @@ msgid "Schedule Activity"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#. odoo-javascript
|
||||
#. odoo-python
|
||||
#: code:addons/mail/static/src/core/web/activity_service.js:0
|
||||
#: code:addons/mail/wizard/mail_activity_schedule.py:0
|
||||
#, python-format
|
||||
@@ -8365,7 +8248,6 @@ msgstr ""
|
||||
msgid "Specific User"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. module: mail
|
||||
#: model:ir.model.fields,help:mail.field_mail_activity_plan__res_model
|
||||
#: model:ir.model.fields,help:mail.field_mail_activity_plan_template__res_model
|
||||
@@ -9188,53 +9070,9 @@ msgstr ""
|
||||
#: code:addons/mail/static/src/core/web/activity_list_popover.xml:0
|
||||
#: code:addons/mail/static/src/core/web/activity_list_popover_item.js:0
|
||||
#: code:addons/mail/static/src/core/web/activity_menu.xml:0
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_journal__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_move__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__account_payment__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__crm_lead__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_booth__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_event__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_registration__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_sponsor__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__event_track__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_contract__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__fleet_vehicle_log_services__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_applicant__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_contract__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_employee__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_expense_sheet__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__hr_leave_allocation__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__lunch_supplier__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity__state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mail_activity_mixin__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mailing_mailing__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_equipment__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__maintenance_request__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_production__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__mrp_unbuild__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__pos_session__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_pricelist__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_product__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__product_template__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_project__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_task__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__project_update__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_order__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__purchase_requisition__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__repair_order__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__res_partner_bank__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__sale_order__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__slide_channel__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_landed_cost__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_lot__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__stock_picking_batch__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_survey__activity_state__today
|
||||
#: model:ir.model.fields.selection,name:mail.selection__survey_user_input__activity_state__today
|
||||
#, python-format
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
@@ -9411,6 +9249,7 @@ msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model:ir.model.fields,field_description:mail.field_mail_link_preview__source_url
|
||||
#: model_terms:ir.ui.view,arch_db:mail.res_config_settings_view_form
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -9609,6 +9448,13 @@ msgstr ""
|
||||
msgid "Upload file"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Upload:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/common/attachment_list.xml:0
|
||||
@@ -10410,6 +10256,13 @@ msgstr ""
|
||||
msgid "attachment(s) of this email."
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "available bitrate:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/gif_picker/common/gif_picker.xml:0
|
||||
@@ -10434,6 +10287,13 @@ msgstr ""
|
||||
msgid "by"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "camera"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.mail_bounce_catchall
|
||||
msgid ""
|
||||
@@ -10448,6 +10308,15 @@ msgstr ""
|
||||
msgid "channels"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "clock rate:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/core/common/search_messages_panel.xml:0
|
||||
@@ -10455,6 +10324,15 @@ msgstr ""
|
||||
msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "codec:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/discuss/discuss_channel.py:0
|
||||
@@ -10509,6 +10387,20 @@ msgstr ""
|
||||
msgid "done"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "down DTLS:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "down ICE:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.mail_activity_type_view_form
|
||||
msgid "e.g. \"Discuss proposal\""
|
||||
@@ -10690,6 +10582,13 @@ msgstr ""
|
||||
msgid "media player Error"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "microphone"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/mail_thread.py:0
|
||||
@@ -10720,7 +10619,6 @@ msgstr ""
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.js:0
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.js:0
|
||||
#, python-format
|
||||
msgid "no connection"
|
||||
msgstr ""
|
||||
@@ -10808,6 +10706,13 @@ msgstr ""
|
||||
msgid "results out of"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "screen"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-python
|
||||
#: code:addons/mail/models/mail_alias.py:0
|
||||
@@ -10867,6 +10772,20 @@ msgstr ""
|
||||
msgid "unknown target model %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "up DTLS:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/call/common/call_context_menu.xml:0
|
||||
#, python-format
|
||||
msgid "up ICE:"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. odoo-javascript
|
||||
#: code:addons/mail/static/src/discuss/core/web/command_palette.js:0
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from ...tools import jwt, discuss
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
SFU_MODE_THRESHOLD = 3
|
||||
|
||||
|
||||
class ChannelMember(models.Model):
|
||||
@@ -212,13 +219,16 @@ class ChannelMember(models.Model):
|
||||
self.rtc_session_ids.unlink()
|
||||
rtc_session = self.env['discuss.channel.rtc.session'].create({'channel_member_id': self.id})
|
||||
current_rtc_sessions, outdated_rtc_sessions = self._rtc_sync_sessions(check_rtc_session_ids=check_rtc_session_ids)
|
||||
ice_servers = self.env["mail.ice.server"]._get_ice_servers()
|
||||
self._join_sfu(ice_servers)
|
||||
res = {
|
||||
'iceServers': self.env['mail.ice.server']._get_ice_servers() or False,
|
||||
'iceServers': ice_servers or False,
|
||||
'rtcSessions': [
|
||||
('ADD', [rtc_session_sudo._mail_rtc_session_format() for rtc_session_sudo in current_rtc_sessions]),
|
||||
('DELETE', [{'id': missing_rtc_session_sudo.id} for missing_rtc_session_sudo in outdated_rtc_sessions]),
|
||||
],
|
||||
'sessionId': rtc_session.id,
|
||||
'serverInfo': self._get_rtc_server_info(rtc_session, ice_servers),
|
||||
}
|
||||
if len(self.channel_id.rtc_session_ids) == 1 and self.channel_id.channel_type in {'chat', 'group'}:
|
||||
self.channel_id.message_post(body=_("%s started a live conference", self.partner_id.name or self.guest_id.name), message_type='notification')
|
||||
@@ -227,6 +237,62 @@ class ChannelMember(models.Model):
|
||||
res['invitedMembers'] = [('ADD', list(invited_members._discuss_channel_member_format(fields={'id': True, 'channel': {}, 'persona': {'partner': {'id', 'name', 'im_status'}, 'guest': {'id', 'name', 'im_status'}}}).values()))]
|
||||
return res
|
||||
|
||||
def _join_sfu(self, ice_servers=None):
|
||||
if len(self.channel_id.rtc_session_ids) < SFU_MODE_THRESHOLD:
|
||||
if self.channel_id.sfu_channel_uuid:
|
||||
self.channel_id.sfu_channel_uuid = None
|
||||
self.channel_id.sfu_server_url = None
|
||||
return
|
||||
elif self.channel_id.sfu_channel_uuid and self.channel_id.sfu_server_url:
|
||||
return
|
||||
sfu_server_url = discuss.get_sfu_url(self.env)
|
||||
if not sfu_server_url:
|
||||
return
|
||||
sfu_server_key = discuss.get_sfu_key(self.env)
|
||||
json_web_token = jwt.sign(
|
||||
{"iss": f"{self.get_base_url()}:channel:{self.channel_id.id}"},
|
||||
key=sfu_server_key,
|
||||
ttl=30,
|
||||
algorithm=jwt.Algorithm.HS256,
|
||||
)
|
||||
try:
|
||||
response = requests.get(
|
||||
sfu_server_url + "/v1/channel",
|
||||
headers={"Authorization": "jwt " + json_web_token},
|
||||
timeout=3,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
_logger.warning("Failed to obtain a channel from the SFU server, user will stay in p2p: %s", error)
|
||||
return
|
||||
response_dict = response.json()
|
||||
self.channel_id.sfu_channel_uuid = response_dict["uuid"]
|
||||
self.channel_id.sfu_server_url = response_dict["url"]
|
||||
notifications = [
|
||||
[
|
||||
session.guest_id or session.partner_id,
|
||||
"discuss.channel.rtc.session/sfu_hot_swap",
|
||||
{"serverInfo": self._get_rtc_server_info(session, ice_servers, key=sfu_server_key)},
|
||||
]
|
||||
for session in self.channel_id.rtc_session_ids
|
||||
]
|
||||
self.env["bus.bus"]._sendmany(notifications)
|
||||
|
||||
def _get_rtc_server_info(self, rtc_session, ice_servers=None, key=None):
|
||||
sfu_channel_uuid = self.channel_id.sfu_channel_uuid
|
||||
sfu_server_url = self.channel_id.sfu_server_url
|
||||
if not sfu_channel_uuid or not sfu_server_url:
|
||||
return None
|
||||
if not key:
|
||||
key = discuss.get_sfu_key(self.env)
|
||||
claims = {
|
||||
"sfu_channel_uuid": sfu_channel_uuid,
|
||||
"session_id": rtc_session.id,
|
||||
"ice_servers": ice_servers,
|
||||
}
|
||||
json_web_token = jwt.sign(claims, key=key, ttl=60 * 60 * 8, algorithm=jwt.Algorithm.HS256) # 8 hours
|
||||
return {"url": sfu_server_url, "jsonWebToken": json_web_token}
|
||||
|
||||
def _rtc_leave_call(self):
|
||||
self.ensure_one()
|
||||
if self.rtc_session_ids:
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from collections import defaultdict
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.mail.tools import discuss, jwt
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailRtcSession(models.Model):
|
||||
@@ -45,6 +51,11 @@ class MailRtcSession(models.Model):
|
||||
# Note: invitation depends on field `rtc_inviting_session_id` so the cancel must be
|
||||
# done before the delete to be able to know who was invited.
|
||||
channel._rtc_cancel_invitations()
|
||||
# If there is no member left in the RTC call, we remove the SFU channel uuid as the SFU
|
||||
# server will timeout the channel. It is better to obtain a new channel from the SFU server
|
||||
# than to attempt recycling a possibly stale channel uuid.
|
||||
channel.sfu_channel_uuid = False
|
||||
channel.sfu_server_url = False
|
||||
notifications = [(channel, 'discuss.channel/rtc_sessions_update', {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('DELETE', [{'id': session_data['id']} for session_data in sessions_data])],
|
||||
@@ -77,6 +88,24 @@ class MailRtcSession(models.Model):
|
||||
self.search(self._inactive_rtc_session_domain()).unlink()
|
||||
|
||||
def action_disconnect(self):
|
||||
session_ids_by_channel_by_url = defaultdict(lambda: defaultdict(list))
|
||||
for rtc_session in self:
|
||||
sfu_channel_uuid = rtc_session.channel_id.sfu_channel_uuid
|
||||
url = rtc_session.channel_id.sfu_server_url
|
||||
if sfu_channel_uuid and url:
|
||||
session_ids_by_channel_by_url[url][sfu_channel_uuid].append(rtc_session.id)
|
||||
key = discuss.get_sfu_key(self.env)
|
||||
if key:
|
||||
with requests.Session() as requests_session:
|
||||
for url, session_ids_by_channel in session_ids_by_channel_by_url.items():
|
||||
try:
|
||||
requests_session.post(
|
||||
url + '/v1/disconnect',
|
||||
data=jwt.sign({'sessionIdsByChannel': session_ids_by_channel}, key=key, ttl=20, algorithm=jwt.Algorithm.HS256),
|
||||
timeout=3
|
||||
).raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
_logger.warning("Could not disconnect sessions at sfu server %s: %s", url, error)
|
||||
self.unlink()
|
||||
|
||||
def _delete_inactive_rtc_sessions(self):
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import logging as logger
|
||||
|
||||
from odoo import api, fields, models
|
||||
from ..web_push import generate_web_push_vapid_key
|
||||
from ..tools.jwt import generate_vapid_keys
|
||||
|
||||
_logger = logger.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +37,7 @@ class PartnerDevice(models.Model):
|
||||
# Regenerate new Keys if public key not present
|
||||
if not public_key_value:
|
||||
self.sudo().search([]).unlink() # Reset all devices (ServiceWorker)
|
||||
private_key_value, public_key_value = generate_web_push_vapid_key()
|
||||
private_key_value, public_key_value = generate_vapid_keys()
|
||||
ir_params_sudo.set_param('mail.web_push_vapid_private_key', private_key_value)
|
||||
ir_params_sudo.set_param(public_key, public_key_value)
|
||||
_logger.info("WebPush: missing public key, new VAPID keys generated")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,424 @@
|
||||
Name: mediasoup-client
|
||||
Version: 3.7.0
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: mediasoup client side JavaScript library
|
||||
Repository: https://github.com/versatica/mediasoup-client.git
|
||||
Homepage: https://mediasoup.org
|
||||
Contributors:
|
||||
Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2015, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: debug
|
||||
Version: 4.3.4
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Lightweight debugging utility for Node.js and the browser
|
||||
Repository: git://github.com/debug-js/debug.git
|
||||
Author: Josh Junon <josh.junon@protonmail.com>
|
||||
Contributors:
|
||||
TJ Holowaychuk <tj@vision-media.ca>
|
||||
Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)
|
||||
Andrew Rhyne <rhyneandrew@gmail.com>
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2018-2021 Josh Junon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the 'Software'), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: ms
|
||||
Version: 2.1.2
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Tiny millisecond conversion utility
|
||||
Repository: undefined
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Zeit, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: ua-parser-js
|
||||
Version: 1.0.37
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data. Supports browser & node.js environment
|
||||
Repository: https://github.com/faisalman/ua-parser-js.git
|
||||
Homepage: https://github.com/faisalman/ua-parser-js
|
||||
Author: Faisal Salman <f@faisalman.com> (http://faisalman.com)
|
||||
Contributors:
|
||||
Aamir Poonawalla <aamir@urx.com>
|
||||
Admas <mollases@users.noreply.github.com>
|
||||
algenon <m@antonz.ru>
|
||||
Alvin Portillo <alvin@8thwall.com>
|
||||
Amumu <yoyo837@hotmail.com>
|
||||
Andrea Vaghi <andrea.vaghi@jobrapido.com>
|
||||
Anton Zhiyanov <m@antonz.ru>
|
||||
Arturo Mejia <amejia@blim.com>
|
||||
Arun Rama Reddy <arunramareddy@gmail.com>
|
||||
Austin Pray <austin@austinpray.com>
|
||||
Bendeguz <symunona@gmail.com>
|
||||
Benjamin Bertrand <bertrand.design@gmail.com>
|
||||
Benjamin Urban <benjamin.urban@sueddeutsche.de>
|
||||
boneyao <admin@boneyao.com>
|
||||
Carl C Von Lewin <carlchristianlewin@gmail.com>
|
||||
CESAR RAMOS <c@imagenproactiva.com>
|
||||
Chad Killingsworth <ckillingsworth@jackhenry.com>
|
||||
Christopher De Cairos <chris.decairos@gmail.com>
|
||||
Cyrille David <cyrille@qonto.eu>
|
||||
Dario Vladovic <d.vladimyr@gmail.com>
|
||||
David Annez <david.annez@gmail.com>
|
||||
Davit Barbakadze <jayarjo@gmail.com>
|
||||
ddivernois <david-emmanuel.divernois@amadeus.com>
|
||||
Deliaz <D3liaz@gmail.com>
|
||||
Demis Palma <demis.palma@gmail.com>
|
||||
dhoko <aurelien@protonmail.com>
|
||||
dianhe <dianhe@webank.com>
|
||||
dineshks1 <dineshks1@d.kumar44@outlook.com>
|
||||
Dmitry Tyschenko <dtyschenko@gmail.com>
|
||||
Douglas Li <doug@knotch.it>
|
||||
Dumitru Uzun <contact@duzun.me>
|
||||
Dustin <duburns@fanatics.com>
|
||||
Elchin Valiyev <elchin.valiyev@autodesk.com>
|
||||
Emil Hesslow <hesslow@gmail.com>
|
||||
Eric Redon <eric.redon@coox.org>
|
||||
Eric Schrenker <eric.schrenker@gmail.com>
|
||||
Erik Hesselink <hesselink@gmail.com>
|
||||
Fabian Becker <halfdan@xnorfz.de>
|
||||
Faisal Salman <f@faisalman.com>
|
||||
Frédéric Camblor <fcamblor@gmail.com>
|
||||
Frederik Ring <frederik.ring@gmail.com>
|
||||
Gerald Host <me@jacobford.co.uk>
|
||||
Germán M. Bravo <german.mb@gmail.com>
|
||||
Grigory Dmitrenko <grigory@snsk.ru>
|
||||
gulpin <gulping.gulpin@gmail.com>
|
||||
Hendrik Helwich <h.helwich@iplabs.de>
|
||||
Hermann Ebert <ebbmo@HE.local>
|
||||
hr6r <hedian@gmail.com>
|
||||
Igor Topal <i.topal@smartweb.com.ua>
|
||||
Ildar Kamalov <i.kamalov@adguard.com>
|
||||
insanehong <insane.hong@navercorp.com>
|
||||
jackpoll <jackpoll123456@gmail.com>
|
||||
Jake Mc <startswithaj@users.noreply.github.com>
|
||||
JBYoshi <12983479+JBYoshi@users.noreply.github.com>
|
||||
Joey Parrish <joeyparrish@google.com>
|
||||
John Tantalo <john.tantalo@gmail.com>
|
||||
John Yanarella <jmy@codecatalyst.com>
|
||||
Jon Buckley <jon@jbuckley.ca>
|
||||
Josh Goldberg <joshuakgoldberg@outlook.com>
|
||||
Junki-Ishida <junki_ishida@dwango.co.jp>
|
||||
Kendall Buchanan <kendall@kendagriff.com>
|
||||
Lee Treveil <leetreveil@gmail.com>
|
||||
leonardo <leofiore@libero.it>
|
||||
Levente Balogh <balogh.levente.hu@gmail.com>
|
||||
Liam Quinn <lquinn@blackberry.com>
|
||||
Lithin <lithin@webklipper.com>
|
||||
ll-syber <670159357@qq.com>
|
||||
Loris Guignard <loris.guignard@gmail.com>
|
||||
Lukas Drgon <lukas.drgon@gmail.com>
|
||||
Lukas Eipert <leipert@users.noreply.github.com>
|
||||
Malash <i@malash.me>
|
||||
Martynas <martynas@firmfirm.co>
|
||||
Masahiko Sato <masasato@yahoo-corp.jp>
|
||||
Matt Brophy <matt@brophy.org>
|
||||
Matthew Origer <karatecowboy@gmail.com>
|
||||
Maximilian Haupt <mail@maximilianhaupt.com>
|
||||
Max Maurer <maxemanuel.maurer@gmail.com>
|
||||
Max Nordlund <max.nordlund@gmail.com>
|
||||
Michael Hess <mhess@connectify.me>
|
||||
MimyyK <michele.marais@hakisa.com>
|
||||
naoh <naoh.cs03g@nctu.edu.tw>
|
||||
Nicholas Ionata <nionata@ufl.edu>
|
||||
Nikhil Motiani <nikhil.motiani@outlook.com>
|
||||
Nik Rolls <nik@rolls.cc>
|
||||
nionata <n.ionata129@gmail.com>
|
||||
niris <nirisix@gmail.com>
|
||||
Nobuo Okada <nookada@yahoo-corp.jp>
|
||||
o.drapeza <o.drapeza@tinkoff.ru>
|
||||
otakuSiD <otakusid@gmail.com>
|
||||
patrick-nurt <github@pereira.dk>
|
||||
Pavel Studeny <studeny@avast.com>
|
||||
Peter Dave Hello <PeterDaveHello@users.noreply.github.com>
|
||||
philippsimon <github@philippsimon.de>
|
||||
Pieter Hendrickx <pieter.hendrickx@belfius.be>
|
||||
Piper Chester <piperchester@gmail.com>
|
||||
Queen Vinyl Darkscratch <vinyldarkscratch@gmail.com>
|
||||
Raine Makelainen <raine.makelainen@jolla.com>
|
||||
Raman Savaryn <homeneartheocean@gmail.com>
|
||||
Robert Tod <robert@qubit.com>
|
||||
roman.savarin <roman.savarin@skywindgroup.com>
|
||||
Ron Korland <ron@testim.io>
|
||||
Ross Noble <rosshnoble@gmail.com>
|
||||
ruicong <466403866@qq.com>
|
||||
Sandro Sonntag <sandro.sonntag@adorsys.de>
|
||||
sgautrea <shanegautreau@gmail.com>
|
||||
Shane Gautreau <sgautrea@opentext.com>
|
||||
Shane Thacker <shane@steadymade.com>
|
||||
Shreedhar <shreedhar@uber.com>
|
||||
Simon Eisenmann <simon@longsleep.org>
|
||||
Simon Lang <me@simonlang.org>
|
||||
Stiekel <histkc@gmail.com>
|
||||
sUP <dani3l@gmail.com>
|
||||
Sylvain Gizard <sylvain.gizard@gmail.com>
|
||||
szchenghuang <szchenghuang@gmail.com>
|
||||
Tanguy Krotoff <tkrotoff@gmail.com>
|
||||
Tony Tomarchio <tony@tomarchio.cc>
|
||||
Ulrich Schmidt <u.schmidt@velian.de>
|
||||
Vadim Kurachevsky <vadim@hmvs.org>
|
||||
Yılmaz <yilmazdemir36@gmail.com>
|
||||
yuanyang <work_yuanyang@163.com>
|
||||
Yun Young-jin <yupmin@yupmin-office-macmini.local>
|
||||
Zach Bjornson <zbbjornson@gmail.com>
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2012-2023 Faisal Salman <<f@faisalman.com>>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: events
|
||||
Version: 3.3.0
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Node's event emitter for all engines.
|
||||
Repository: git://github.com/Gozala/events.git
|
||||
Author: Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
MIT
|
||||
|
||||
Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: h264-profile-level-id
|
||||
Version: 1.0.2
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: Utility to process H264 profile-level-id values
|
||||
Repository: https://github.com/ibc/h264-profile-level-id.git
|
||||
Author: Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2019, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: awaitqueue
|
||||
Version: 3.0.2
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: JavaScript utility to enqueue asynchronous tasks and run them sequentially one after another
|
||||
Repository: https://github.com/versatica/awaitqueue.git
|
||||
Author: Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2019, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: queue-microtask
|
||||
Version: 1.2.3
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: fast, tiny `queueMicrotask` shim for modern engines
|
||||
Repository: git://github.com/feross/queue-microtask.git
|
||||
Homepage: https://github.com/feross/queue-microtask
|
||||
Author: Feross Aboukhadijeh <feross@feross.org> (https://feross.org)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: sdp-transform
|
||||
Version: 2.14.1
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: A simple parser/writer for the Session Description Protocol
|
||||
Repository: clux/sdp-transform
|
||||
Author: Eirik Albrigtsen <sszynrae@gmail.com>
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Eirik Albrigtsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -6,6 +6,8 @@ import { browser } from "@web/core/browser/browser";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { CONNECTION_TYPES } from "@mail/discuss/call/common/rtc_service";
|
||||
|
||||
const PROTOCOLS_TEXT = { host: "HOST", srflx: "STUN", prflx: "STUN", relay: "TURN" };
|
||||
|
||||
export class CallContextMenu extends Component {
|
||||
@@ -13,46 +15,125 @@ export class CallContextMenu extends Component {
|
||||
static template = "discuss.CallContextMenu";
|
||||
|
||||
updateStatsTimeout;
|
||||
rtcConnectionTypes = CONNECTION_TYPES;
|
||||
|
||||
setup() {
|
||||
this.userSettings = useState(useService("mail.user_settings"));
|
||||
this.rtc = useState(useService("discuss.rtc"));
|
||||
this.state = useState({
|
||||
downloadStats: {},
|
||||
uploadStats: {},
|
||||
producerStats: {},
|
||||
});
|
||||
onMounted(() => {
|
||||
if (!this.env.debug) {
|
||||
return;
|
||||
}
|
||||
this.props.rtcSession.updateStats();
|
||||
this.updateStatsTimeout = browser.setInterval(
|
||||
() => this.props.rtcSession.updateStats(),
|
||||
3000
|
||||
);
|
||||
this.updateStats();
|
||||
this.updateStatsTimeout = browser.setInterval(() => this.updateStats(), 3000);
|
||||
});
|
||||
onWillUnmount(() => browser.clearInterval(this.updateStatsTimeout));
|
||||
}
|
||||
|
||||
get isSelf() {
|
||||
return this.rtc.state.selfSession?.eq(this.props.rtcSession);
|
||||
}
|
||||
|
||||
get inboundConnectionTypeText() {
|
||||
if (!this.props.rtcSession.remoteCandidateType) {
|
||||
return _t("no connection");
|
||||
}
|
||||
return _t("%(candidateType)s (%(protocol)s)", {
|
||||
candidateType: this.props.rtcSession.remoteCandidateType,
|
||||
protocol: PROTOCOLS_TEXT[this.props.rtcSession.remoteCandidateType],
|
||||
});
|
||||
const candidateType =
|
||||
this.rtc.state.connectionType === CONNECTION_TYPES.SERVER
|
||||
? this.state.downloadStats.remoteCandidateType
|
||||
: this.props.rtcSession.remoteCandidateType;
|
||||
return this.formatProtocol(candidateType);
|
||||
}
|
||||
|
||||
get outboundConnectionTypeText() {
|
||||
if (!this.props.rtcSession.localCandidateType) {
|
||||
return _t("no connection");
|
||||
}
|
||||
return _t("%(candidateType)s (%(protocol)s)", {
|
||||
candidateType: this.props.rtcSession.localCandidateType,
|
||||
protocol: PROTOCOLS_TEXT[this.props.rtcSession.localCandidateType],
|
||||
});
|
||||
const candidateType =
|
||||
this.rtc.state.connectionType === CONNECTION_TYPES.SERVER
|
||||
? this.state.uploadStats.localCandidateType
|
||||
: this.props.rtcSession.localCandidateType;
|
||||
return this.formatProtocol(candidateType);
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.userSettings.getVolume(this.props.rtcSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} candidateType
|
||||
* @returns {string} a formatted string that describes the connection type e.g: "prflx (STUN)"
|
||||
*/
|
||||
formatProtocol(candidateType) {
|
||||
if (!candidateType) {
|
||||
return _t("no connection");
|
||||
}
|
||||
return _t("%(candidateType)s (%(protocol)s)", {
|
||||
candidateType,
|
||||
protocol: PROTOCOLS_TEXT[candidateType],
|
||||
});
|
||||
}
|
||||
|
||||
async updateStats() {
|
||||
if (this.rtc.state.selfSession?.eq(this.props.rtcSession)) {
|
||||
if (this.rtc.sfuClient) {
|
||||
const { uploadStats, downloadStats, ...producerStats } =
|
||||
await this.rtc.sfuClient.getStats();
|
||||
const formattedUploadStats = {};
|
||||
for (const value of uploadStats.values?.() || []) {
|
||||
switch (value.type) {
|
||||
case "candidate-pair":
|
||||
if (value.state === "succeeded" && value.localCandidateId) {
|
||||
formattedUploadStats.localCandidateType =
|
||||
uploadStats.get(value.localCandidateId)?.candidateType || "";
|
||||
formattedUploadStats.availableOutgoingBitrate =
|
||||
value.availableOutgoingBitrate;
|
||||
}
|
||||
break;
|
||||
case "transport":
|
||||
formattedUploadStats.dtlsState = value.dtlsState;
|
||||
formattedUploadStats.iceState = value.iceState;
|
||||
formattedUploadStats.packetsSent = value.packetsSent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const formattedDownloadStats = {};
|
||||
for (const value of downloadStats.values?.() || []) {
|
||||
switch (value.type) {
|
||||
case "candidate-pair":
|
||||
if (value.state === "succeeded" && value.localCandidateId) {
|
||||
formattedDownloadStats.remoteCandidateType =
|
||||
downloadStats.get(value.remoteCandidateId)?.candidateType || "";
|
||||
}
|
||||
break;
|
||||
case "transport":
|
||||
formattedDownloadStats.dtlsState = value.dtlsState;
|
||||
formattedDownloadStats.iceState = value.iceState;
|
||||
formattedDownloadStats.packetsReceived = value.packetsReceived;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const formattedProducerStats = {};
|
||||
for (const [type, stat] of Object.entries(producerStats)) {
|
||||
const currentTypeStats = {};
|
||||
for (const value of stat.values()) {
|
||||
switch (value.type) {
|
||||
case "codec":
|
||||
currentTypeStats.codec = value.mimeType;
|
||||
currentTypeStats.clockRate = value.clockRate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
formattedProducerStats[type] = currentTypeStats;
|
||||
}
|
||||
this.state.uploadStats = formattedUploadStats;
|
||||
this.state.downloadStats = formattedDownloadStats;
|
||||
this.state.producerStats = formattedProducerStats;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await this.props.rtcSession.updateStats();
|
||||
}
|
||||
|
||||
onChangeVolume(ev) {
|
||||
const volume = Number(ev.target.value);
|
||||
this.userSettings.saveVolumeSetting({
|
||||
|
||||
@@ -2,22 +2,56 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="discuss.CallContextMenu">
|
||||
<div class="d-flex flex-column p-3">
|
||||
<input type="range" min="0.0" max="1" step="0.01" t-att-value="volume" t-on-change="onChangeVolume" class="form-range"/>
|
||||
<t t-if="env.debug">
|
||||
<input t-if="!isSelf" type="range" min="0.0" max="1" step="0.01" t-att-value="volume" t-on-change="onChangeVolume" class="form-range"/>
|
||||
<t t-if="env.debug and !isSelf and rtc.state.connectionType === rtcConnectionTypes.P2P">
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">Connection: </span><t t-esc="props.rtcSession.connectionState"/></div>
|
||||
<div><span class="fw-bolder">ICE: </span><t t-esc="props.rtcSession.iceState"/></div>
|
||||
<div><span class="fw-bolder">DTLS: </span><t t-esc="props.rtcSession.dtlsState"/></div>
|
||||
<div><span class="fw-bolder">Data channel: </span><t t-esc="props.rtcSession.dataChannelState"/></div>
|
||||
<div t-if="props.rtcSession.audioError"><span class="fw-bolder">Audio player: </span><t t-esc="props.rtcSession.audioError"/></div>
|
||||
<div t-if="props.rtcSession.videoError"><span class="fw-bolder">Video player: </span><t t-esc="props.rtcSession.videoError"/></div>
|
||||
<div><span class="fw-bolder">Connection type: </span><t t-out="rtc.state.connectionType"/></div>
|
||||
<div><span class="fw-bolder">To peer: </span><t t-out="outboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">From peer: </span><t t-out="inboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">Connection: </span><t t-out="props.rtcSession.connectionState"/></div>
|
||||
<div><span class="fw-bolder">ICE: </span><t t-out="props.rtcSession.iceState"/></div>
|
||||
<div><span class="fw-bolder">DTLS: </span><t t-out="props.rtcSession.dtlsState"/></div>
|
||||
<div><span class="fw-bolder">Data channel: </span><t t-out="props.rtcSession.dataChannelState"/></div>
|
||||
<div t-if="props.rtcSession.audioError"><span class="fw-bolder">Audio player: </span><t t-out="props.rtcSession.audioError"/></div>
|
||||
<div t-if="props.rtcSession.videoError"><span class="fw-bolder">Video player: </span><t t-out="props.rtcSession.videoError"/></div>
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">ICE gathering: </span><t t-esc="props.rtcSession.iceGatheringState"/></div>
|
||||
<div><span class="fw-bolder">From peer: </span><t t-esc="inboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">To peer: </span><t t-esc="outboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">Packets sent: </span><t t-esc="props.rtcSession.packetsSent"/></div>
|
||||
<div><span class="fw-bolder">Packets received: </span><t t-esc="props.rtcSession.packetsReceived"/></div>
|
||||
<div><span class="fw-bolder">Log step: </span><t t-esc="props.rtcSession.logStep"/></div>
|
||||
<div><span class="fw-bolder">ICE gathering: </span><t t-out="props.rtcSession.iceGatheringState"/></div>
|
||||
<div><span class="fw-bolder">Packets sent: </span><t t-out="props.rtcSession.packetsSent"/></div>
|
||||
<div><span class="fw-bolder">Packets received: </span><t t-out="props.rtcSession.packetsReceived"/></div>
|
||||
<div><span class="fw-bolder">Log step: </span><t t-out="props.rtcSession.logStep"/></div>
|
||||
</t>
|
||||
<t t-elif="env.debug and isSelf and rtc.state.connectionType === rtcConnectionTypes.SERVER">
|
||||
<div><span class="fw-bolder">Connection type: </span><t t-out="rtc.state.connectionType"/></div>
|
||||
<div><span class="fw-bolder">RTC Session ID: </span><t t-out="props.rtcSession.id"/></div>
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">Upload: </span><t t-out="outboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">up ICE: </span><t t-out="state.uploadStats.iceState"/></div>
|
||||
<div><span class="fw-bolder">up DTLS: </span><t t-out="state.uploadStats.dtlsState"/></div>
|
||||
<div><span class="fw-bolder">Packets sent: </span><t t-out="state.uploadStats.packetsSent"/></div>
|
||||
<div><span class="fw-bolder">available bitrate: </span><t t-out="state.uploadStats.availableOutgoingBitrate"/></div>
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">Download: </span><t t-out="inboundConnectionTypeText"/></div>
|
||||
<div><span class="fw-bolder">down ICE: </span><t t-out="state.downloadStats.iceState"/></div>
|
||||
<div><span class="fw-bolder">down DTLS: </span><t t-out="state.downloadStats.dtlsState"/></div>
|
||||
<div><span class="fw-bolder">Packets received: </span><t t-out="state.downloadStats.packetsReceived"/></div>
|
||||
<t t-if="state.producerStats.audio">
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">microphone</span></div>
|
||||
<div><span class="fw-bolder">codec: </span><t t-out="state.producerStats.audio.codec"/></div>
|
||||
<div><span class="fw-bolder">clock rate: </span><t t-out="state.producerStats.audio.clockRate"/></div>
|
||||
</t>
|
||||
<t t-if="state.producerStats.camera and props.rtcSession.isCameraOn">
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">camera</span></div>
|
||||
<div><span class="fw-bolder">codec: </span><t t-out="state.producerStats.camera.codec"/></div>
|
||||
<div><span class="fw-bolder">clock rate: </span><t t-out="state.producerStats.camera.clockRate"/></div>
|
||||
</t>
|
||||
<t t-if="state.producerStats.screen and props.rtcSession.isScreenSharingOn">
|
||||
<hr class="w-100 border-top"/>
|
||||
<div><span class="fw-bolder">screen</span></div>
|
||||
<div><span class="fw-bolder">codec: </span><t t-out="state.producerStats.screen.codec"/></div>
|
||||
<div><span class="fw-bolder">clock rate: </span><t t-out="state.producerStats.screen.clockRate"/></div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -59,6 +59,9 @@ export class CallParticipantCard extends Component {
|
||||
if (!this.rtcSession) {
|
||||
return false;
|
||||
}
|
||||
if (this.env.debug) {
|
||||
return true;
|
||||
}
|
||||
return !this.rtcSession?.eq(this.rtc.state.selfSession);
|
||||
}
|
||||
|
||||
@@ -77,11 +80,19 @@ export class CallParticipantCard extends Component {
|
||||
get showConnectionState() {
|
||||
return Boolean(
|
||||
this.isOfActiveCall &&
|
||||
!this.rtcSession.channelMember?.persona.eq(this.store.self) &&
|
||||
this.rtcSession?.peerConnection &&
|
||||
!HIDDEN_CONNECTION_STATES.has(this.rtcSession.connectionState)
|
||||
);
|
||||
}
|
||||
|
||||
get showServerState() {
|
||||
return Boolean(
|
||||
this.rtcSession.channelMember?.persona.eq(this.store.self) &&
|
||||
this.rtc.state.serverState &&
|
||||
this.rtc.state.serverState !== "connected"
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.channelMember?.persona.name;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
<span t-if="showConnectionState" class="d-flex flex-column justify-content-center me-1 p-2 rounded-circle o-discuss-CallParticipantCard-iconBlackBg" t-att-title="rtcSession.connectionState">
|
||||
<i class="fa fa-exclamation-triangle text-warning"/>
|
||||
</span>
|
||||
<span t-if="showServerState" class="d-flex flex-column justify-content-center me-1 p-2 rounded-circle o-discuss-CallParticipantCard-iconBlackBg" t-att-title="rtc.state.serverState">
|
||||
<i class="fa fa-exclamation-triangle text-warning"/>
|
||||
</span>
|
||||
<span t-if="rtcSession.isScreenSharingOn and !props.minimized and !isOfActiveCall" class="user-select-none rounded-pill text-bg-danger d-flex align-items-center me-1 fw-bolder" title="live" aria-label="live">
|
||||
LIVE
|
||||
</span>
|
||||
|
||||
@@ -73,7 +73,7 @@ function _loadScriptProcessor(
|
||||
audioContext,
|
||||
{
|
||||
frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE,
|
||||
minimumActiveCycles = 10,
|
||||
minimumActiveCycles = 30,
|
||||
onThreshold,
|
||||
onTic,
|
||||
volumeThreshold = 0.3,
|
||||
|
||||
@@ -10,12 +10,30 @@ import { browser } from "@web/core/browser/browser";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { loadBundle } from "@web/core/assets";
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
|
||||
/**
|
||||
* @return {Promise<{ SfuClient: import("@mail/static/libs/odoo_sfu/odoo_sfu").SfuClient, SFU_CLIENT_STATE: import("@mail/static/libs/odoo_sfu/odoo_sfu").SFU_CLIENT_STATE }>}
|
||||
*/
|
||||
const loadSfuAssets = memoize(async () => await loadBundle("mail.assets_odoo_sfu"));
|
||||
|
||||
const ORDERED_TRANSCEIVER_NAMES = ["audio", "screen", "camera"];
|
||||
export const CONNECTION_TYPES = { P2P: "p2p", SERVER: "server" };
|
||||
const PEER_NOTIFICATION_WAIT_DELAY = 50;
|
||||
const RECOVERY_TIMEOUT = 15_000;
|
||||
const RECOVERY_DELAY = 3_000;
|
||||
const VIDEO_CONFIG = {
|
||||
const SCREEN_CONFIG = {
|
||||
width: { max: 1920 },
|
||||
height: { max: 1080 },
|
||||
aspectRatio: 16 / 9,
|
||||
frameRate: {
|
||||
max: 24,
|
||||
},
|
||||
};
|
||||
const CAMERA_CONFIG = {
|
||||
width: { max: 1280 },
|
||||
height: { max: 720 },
|
||||
aspectRatio: 16 / 9,
|
||||
frameRate: {
|
||||
max: 30,
|
||||
@@ -86,7 +104,12 @@ function hasTurn(iceServers) {
|
||||
|
||||
export class Rtc {
|
||||
notifications = reactive(new Map());
|
||||
/** @type {Map<string, number>} timeoutId by notificationId for call notifications */
|
||||
timeouts = new Map();
|
||||
/** @type {Map<number, number>} timeoutId by sessionId for download pausing delay */
|
||||
downloadTimeouts = new Map();
|
||||
/** @type {import("@mail/static/libs/odoo_sfu/odoo_sfu").SfuClient} */
|
||||
sfuClient = undefined;
|
||||
|
||||
/**
|
||||
* @param {import("@web/env").OdooEnv} env
|
||||
@@ -100,7 +123,10 @@ export class Rtc {
|
||||
this.soundEffectsService = services["mail.sound_effects"];
|
||||
this.userSettingsService = services["mail.user_settings"];
|
||||
this.pttExtService = services["discuss.ptt_extension"];
|
||||
this._handleSfuClientUpdates = this._handleSfuClientUpdates.bind(this);
|
||||
this._handleSfuClientStateChange = this._handleSfuClientStateChange.bind(this);
|
||||
this.state = reactive({
|
||||
connectionType: undefined,
|
||||
hasPendingRequest: false,
|
||||
selfSession: undefined,
|
||||
channel: undefined,
|
||||
@@ -108,6 +134,8 @@ export class Rtc {
|
||||
logs: new Map(),
|
||||
sendCamera: false,
|
||||
sendScreen: false,
|
||||
serverInfo: undefined,
|
||||
serverState: undefined,
|
||||
updateAndBroadcastDebounce: undefined,
|
||||
isPendingNotify: false,
|
||||
notificationsToSend: new Map(),
|
||||
@@ -196,6 +224,7 @@ export class Rtc {
|
||||
// browser prevents async requests from firing when the browser
|
||||
// is closed. Alternatives like synchronous XHR are not reliable.
|
||||
browser.navigator.sendBeacon("/mail/rtc/channel/leave_call", blob);
|
||||
this.sfuClient?.disconnect();
|
||||
}
|
||||
});
|
||||
/**
|
||||
@@ -203,8 +232,8 @@ export class Rtc {
|
||||
* a regular interval to try to recover any connection that failed
|
||||
* to start.
|
||||
*
|
||||
* This is distinct from this.recover which tries to restores
|
||||
* connection that were established but failed or timed out.
|
||||
* This is distinct from this.recover which tries to restore
|
||||
* connections that were established but failed or timed out.
|
||||
*/
|
||||
browser.setInterval(async () => {
|
||||
if (!this.state.selfSession || !this.state.channel) {
|
||||
@@ -241,6 +270,32 @@ export class Rtc {
|
||||
}
|
||||
this.setTalking(true);
|
||||
}
|
||||
|
||||
async loadSfuClient() {
|
||||
this.state.connectionType = CONNECTION_TYPES.P2P;
|
||||
if (!this.state.serverInfo) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadSfuAssets();
|
||||
const sfuModule = odoo.loader.modules.get("@mail/../lib/odoo_sfu/odoo_sfu");
|
||||
this.disconnectFromSfu();
|
||||
this.SFU_CLIENT_STATE = sfuModule.SFU_CLIENT_STATE;
|
||||
this.sfuClient = new sfuModule.SfuClient();
|
||||
this.sfuClient.addEventListener("update", this._handleSfuClientUpdates);
|
||||
this.sfuClient.addEventListener("stateChange", this._handleSfuClientStateChange);
|
||||
this.state.connectionType = CONNECTION_TYPES.SERVER;
|
||||
} catch (e) {
|
||||
this.notification.add(
|
||||
_t("Failed to load the SFU server, falling back to peer-to-peer"),
|
||||
{
|
||||
type: "warning",
|
||||
}
|
||||
);
|
||||
this.log(this.state.selfSession, "failed to load sfu server", { error: e });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {any} param0.id
|
||||
@@ -274,8 +329,19 @@ export class Rtc {
|
||||
* Notifies the server and does the cleanup of the current call.
|
||||
*/
|
||||
async leaveCall(channel = this.state.channel) {
|
||||
this.state.hasPendingRequest = true;
|
||||
await this.rpcLeaveCall(channel);
|
||||
this.endCall(channel);
|
||||
this.state.hasPendingRequest = false;
|
||||
}
|
||||
|
||||
disconnectFromSfu() {
|
||||
if (!this.sfuClient) {
|
||||
return;
|
||||
}
|
||||
this.sfuClient.removeEventListener("update", this._handleSfuClientUpdates);
|
||||
this.sfuClient.removeEventListener("stateChange", this._handleSfuClientStateChange);
|
||||
this.sfuClient.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +352,7 @@ export class Rtc {
|
||||
channel.activeRtcSession = undefined;
|
||||
if (channel.eq(this.state.channel)) {
|
||||
this.pttExtService.unsubscribe();
|
||||
this.disconnectFromSfu();
|
||||
this.clear();
|
||||
this.soundEffectsService.play("channel-leave");
|
||||
}
|
||||
@@ -296,9 +363,28 @@ export class Rtc {
|
||||
this.soundEffectsService.play("deafen");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("@mail/discuss/call/common/rtc_session_model").RtcSession} session
|
||||
* @param {boolean} active
|
||||
*/
|
||||
setRemoteRaiseHand(session, active) {
|
||||
Object.assign(session, {
|
||||
raisingHand: active ? new Date() : undefined,
|
||||
});
|
||||
const notificationId = "raise_hand_" + session.id;
|
||||
if (session.raisingHand) {
|
||||
this.addCallNotification({
|
||||
id: notificationId,
|
||||
text: _t("%s raised their hand", session.name),
|
||||
});
|
||||
} else {
|
||||
this.removeCallNotification(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
async handleNotification(sessionId, content) {
|
||||
const { event, channelId, payload } = JSON.parse(content);
|
||||
const session = this.state.channel?.rtcSessions.find((session) => session.id === sessionId);
|
||||
const session = this.store.RtcSession.get(sessionId);
|
||||
if (
|
||||
!session ||
|
||||
!IS_CLIENT_RTC_COMPATIBLE ||
|
||||
@@ -418,19 +504,7 @@ export class Rtc {
|
||||
this.disconnect(session);
|
||||
break;
|
||||
case "raise_hand":
|
||||
Object.assign(session, {
|
||||
raisingHand: payload.active ? new Date() : undefined,
|
||||
});
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const notificationId = "raise_hand_" + session.id;
|
||||
if (session.raisingHand) {
|
||||
this.addCallNotification({
|
||||
id: notificationId,
|
||||
text: _t("%s raised a hand", session.name),
|
||||
});
|
||||
} else {
|
||||
this.removeCallNotification(notificationId);
|
||||
}
|
||||
this.setRemoteRaiseHand(session, payload.active);
|
||||
break;
|
||||
case "trackChange": {
|
||||
if (payload.type === "audio") {
|
||||
@@ -474,7 +548,6 @@ export class Rtc {
|
||||
if (this.state.hasPendingRequest) {
|
||||
return;
|
||||
}
|
||||
this.state.hasPendingRequest = true;
|
||||
const isActiveCall = channel.eq(this.state.channel);
|
||||
if (this.state.channel) {
|
||||
await this.leaveCall(this.state.channel);
|
||||
@@ -482,7 +555,6 @@ export class Rtc {
|
||||
if (!isActiveCall) {
|
||||
await this.joinCall(channel, { video });
|
||||
}
|
||||
this.state.hasPendingRequest = false;
|
||||
}
|
||||
|
||||
async toggleMicrophone() {
|
||||
@@ -554,7 +626,109 @@ export class Rtc {
|
||||
this.state.outgoingSessions.add(session.id);
|
||||
}
|
||||
|
||||
call() {
|
||||
/**
|
||||
* @param {CustomEvent} param0
|
||||
* @param {Object} param0.detail
|
||||
* @param {String} param0.detail.name
|
||||
* @param {any} param0.detail.payload
|
||||
*/
|
||||
async _handleSfuClientUpdates({ detail: { name, payload } }) {
|
||||
if (!this.state.channel) {
|
||||
return;
|
||||
}
|
||||
switch (name) {
|
||||
case "disconnect":
|
||||
{
|
||||
const { sessionId } = payload;
|
||||
const session = this.store.RtcSession.get(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
this.disconnect(session);
|
||||
}
|
||||
return;
|
||||
case "info_change":
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
for (const [id, info] of Object.entries(payload)) {
|
||||
const session = this.store.RtcSession.get(Number(id));
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
// `isRaisingHand` is turned into the Date `raisingHand`
|
||||
this.setRemoteRaiseHand(session, info.isRaisingHand);
|
||||
delete info.isRaisingHand;
|
||||
Object.assign(session, info);
|
||||
}
|
||||
return;
|
||||
case "track":
|
||||
{
|
||||
const { sessionId, type, track, active } = payload;
|
||||
const session = this.store.RtcSession.get(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.handleRemoteTrack({ session, track, type, active });
|
||||
} catch {
|
||||
// ignored, the session may be closing.
|
||||
// this can happen when you join a call from another tab in which you have another session.
|
||||
}
|
||||
// makes sure we are not downloading a video that is not displayed
|
||||
setTimeout(() => {
|
||||
this.updateVideoDownload(session);
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async _handleSfuClientStateChange({ detail: { state, cause } }) {
|
||||
this.state.serverState = state;
|
||||
switch (state) {
|
||||
case this.SFU_CLIENT_STATE.AUTHENTICATED:
|
||||
// if we are hot-swapping connection type, we clear the p2p as late as possible
|
||||
this.clearPeerToPeer();
|
||||
break;
|
||||
case this.SFU_CLIENT_STATE.CONNECTED:
|
||||
this.sfuClient.updateInfo(this.formatInfo(), {
|
||||
needRefresh: true, // asks the server to send the info from all the channel
|
||||
});
|
||||
this.sfuClient.updateUpload("audio", this.state.audioTrack);
|
||||
this.sfuClient.updateUpload("camera", this.state.cameraTrack);
|
||||
this.sfuClient.updateUpload("screen", this.state.screenTrack);
|
||||
return;
|
||||
case this.SFU_CLIENT_STATE.CLOSED:
|
||||
{
|
||||
let text;
|
||||
if (cause === "full") {
|
||||
text = _t("Channel full");
|
||||
} else {
|
||||
text = _t("Connection to SFU server closed by the server");
|
||||
}
|
||||
this.notification.add(text, {
|
||||
type: "warning",
|
||||
});
|
||||
await this.leaveCall();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async call() {
|
||||
if (this.state.connectionType === CONNECTION_TYPES.SERVER) {
|
||||
if (this.sfuClient.state === this.SFU_CLIENT_STATE.DISCONNECTED) {
|
||||
await this.sfuClient.connect(
|
||||
this.state.serverInfo.url,
|
||||
this.state.serverInfo.jsonWebToken,
|
||||
{
|
||||
iceServers: this.state.iceServers,
|
||||
}
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.state.channel.rtcSessions.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -730,7 +904,8 @@ export class Rtc {
|
||||
return;
|
||||
}
|
||||
this.pttExtService.subscribe();
|
||||
const { rtcSessions, iceServers, sessionId } = await this.rpc(
|
||||
this.state.hasPendingRequest = true;
|
||||
const { rtcSessions, iceServers, sessionId, serverInfo } = await this.rpc(
|
||||
"/mail/rtc/channel/join_call",
|
||||
{
|
||||
channel_id: channel.id,
|
||||
@@ -742,6 +917,7 @@ export class Rtc {
|
||||
this.clear();
|
||||
this.state.logs.clear();
|
||||
this.state.channel = channel;
|
||||
this.state.serverInfo = serverInfo;
|
||||
this.state.channel.rtcSessions = rtcSessions;
|
||||
this.state.selfSession = this.store.RtcSession.get(sessionId);
|
||||
this.state.iceServers = iceServers || DEFAULT_ICE_SERVERS;
|
||||
@@ -790,12 +966,14 @@ export class Rtc {
|
||||
true
|
||||
);
|
||||
this.state.channel.rtcInvitingSession = undefined;
|
||||
this.call();
|
||||
await this.loadSfuClient();
|
||||
await this.call();
|
||||
this.soundEffectsService.play("channel-join");
|
||||
await this.resetAudioTrack({ force: true });
|
||||
if (video) {
|
||||
await this.toggleVideo("camera");
|
||||
}
|
||||
this.state.hasPendingRequest = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -908,6 +1086,11 @@ export class Rtc {
|
||||
}
|
||||
|
||||
disconnect(session) {
|
||||
const downloadTimeout = this.downloadTimeouts.get(session.id);
|
||||
if (downloadTimeout) {
|
||||
clearTimeout(downloadTimeout);
|
||||
this.downloadTimeouts.delete(session.id);
|
||||
}
|
||||
this.removeCallNotification("raise_hand_" + session.id);
|
||||
closeStream(session.audioStream);
|
||||
if (session.audioElement) {
|
||||
@@ -962,20 +1145,29 @@ export class Rtc {
|
||||
this.log(session, "peer removed", { step: "peer removed" });
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearPeerToPeer() {
|
||||
for (const session of Object.values(this.store.RtcSession.records)) {
|
||||
if (session === this.state.selfSession) {
|
||||
continue;
|
||||
}
|
||||
this.disconnect(session);
|
||||
}
|
||||
for (const timeoutId of this.state.recoverTimeouts.values()) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
this.state.recoverTimeouts.clear();
|
||||
this.state.notificationsToSend.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.clearPeerToPeer();
|
||||
this.sfuClient = undefined;
|
||||
this.state.serverState = undefined;
|
||||
this.state.updateAndBroadcastDebounce?.cancel();
|
||||
this.state.disconnectAudioMonitor?.();
|
||||
this.state.audioTrack?.stop();
|
||||
this.state.cameraTrack?.stop();
|
||||
this.state.screenTrack?.stop();
|
||||
this.state.notificationsToSend.clear();
|
||||
closeStream(this.state.sourceCameraStream);
|
||||
this.state.sourceCameraStream = null;
|
||||
if (this.blurManager) {
|
||||
@@ -984,6 +1176,7 @@ export class Rtc {
|
||||
}
|
||||
Object.assign(this.state, {
|
||||
updateAndBroadcastDebounce: undefined,
|
||||
connectionType: undefined,
|
||||
disconnectAudioMonitor: undefined,
|
||||
outgoingSessions: new Set(),
|
||||
cameraTrack: undefined,
|
||||
@@ -992,6 +1185,7 @@ export class Rtc {
|
||||
selfSession: undefined,
|
||||
sendCamera: false,
|
||||
sendScreen: false,
|
||||
serverInfo: undefined,
|
||||
channel: undefined,
|
||||
});
|
||||
}
|
||||
@@ -1127,11 +1321,16 @@ export class Rtc {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const session of this.state.channel.rtcSessions) {
|
||||
if (session.eq(this.state.selfSession)) {
|
||||
continue;
|
||||
const updatedTrack = type === "camera" ? this.state.cameraTrack : this.state.screenTrack;
|
||||
if (this.sfuClient) {
|
||||
await this.sfuClient.updateUpload(type, updatedTrack);
|
||||
} else {
|
||||
for (const session of this.state.channel.rtcSessions) {
|
||||
if (session.eq(this.state.selfSession)) {
|
||||
continue;
|
||||
}
|
||||
await this.updateRemote(session, type);
|
||||
}
|
||||
await this.updateRemote(session, type);
|
||||
}
|
||||
if (!this.state.selfSession) {
|
||||
return;
|
||||
@@ -1168,6 +1367,9 @@ export class Rtc {
|
||||
}
|
||||
this.state.audioTrack.enabled =
|
||||
!this.state.selfSession.isMute && this.state.selfSession.isTalking;
|
||||
if (this.sfuClient && this.sfuClient.state === this.SFU_CLIENT_STATE.CONNECTED) {
|
||||
this.sfuClient.updateInfo(this.formatInfo());
|
||||
}
|
||||
await this.notify(this.state.channel.rtcSessions, "trackChange", {
|
||||
type: "audio",
|
||||
state: {
|
||||
@@ -1219,7 +1421,7 @@ export class Rtc {
|
||||
sourceStream = this.state.sourceCameraStream;
|
||||
} else {
|
||||
sourceStream = await browser.navigator.mediaDevices.getUserMedia({
|
||||
video: VIDEO_CONFIG,
|
||||
video: CAMERA_CONFIG,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1228,7 +1430,7 @@ export class Rtc {
|
||||
sourceStream = this.state.sourceScreenStream;
|
||||
} else {
|
||||
sourceStream = await browser.navigator.mediaDevices.getDisplayMedia({
|
||||
video: VIDEO_CONFIG,
|
||||
video: SCREEN_CONFIG,
|
||||
});
|
||||
}
|
||||
this.soundEffectsService.play("screen-sharing");
|
||||
@@ -1369,6 +1571,10 @@ export class Rtc {
|
||||
audioTrack.enabled = !this.state.selfSession.isMute && this.state.selfSession.isTalking;
|
||||
this.state.audioTrack = audioTrack;
|
||||
await this.linkVoiceActivation();
|
||||
if (this.sfuClient) {
|
||||
await this.sfuClient.updateUpload("audio", this.state.audioTrack);
|
||||
return;
|
||||
}
|
||||
for (const session of this.state.channel.rtcSessions) {
|
||||
if (session.eq(this.state.selfSession)) {
|
||||
continue;
|
||||
@@ -1431,6 +1637,12 @@ export class Rtc {
|
||||
}
|
||||
}
|
||||
|
||||
formatInfo() {
|
||||
this.state.selfSession.isCameraOn = Boolean(this.state.cameraTrack);
|
||||
this.state.selfSession.isScreenSharingOn = Boolean(this.state.screenTrack);
|
||||
return this.state.selfSession.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RtcSession} session
|
||||
* @param {MediaStreamTrack} track
|
||||
@@ -1517,8 +1729,41 @@ export class Rtc {
|
||||
}
|
||||
}
|
||||
|
||||
updateVideoDownload(rtcSession, { viewCountIncrement }) {
|
||||
/**
|
||||
* @param {import("@mail/discuss/call/common/rtc_session_model").RtcSession} rtcSession
|
||||
* @param {Object} [param1]
|
||||
* @param {number} [param1.viewCountIncrement=0] negative value to decrement
|
||||
*/
|
||||
updateVideoDownload(rtcSession, { viewCountIncrement = 0 } = {}) {
|
||||
rtcSession.videoComponentCount += viewCountIncrement;
|
||||
const downloadTimeout = this.downloadTimeouts.get(rtcSession.id);
|
||||
if (downloadTimeout) {
|
||||
this.downloadTimeouts.delete(rtcSession.id);
|
||||
browser.clearTimeout(downloadTimeout);
|
||||
}
|
||||
if (this.state.connectionType === CONNECTION_TYPES.SERVER) {
|
||||
if (rtcSession.videoComponentCount > 0) {
|
||||
this.sfuClient?.updateDownload(rtcSession.id, {
|
||||
camera: true,
|
||||
screen: true,
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* We wait a bit before pausing a download to avoid flickering, if the user stops downloading and starts again
|
||||
* soon after, it is not worth pausing the download.
|
||||
*/
|
||||
this.downloadTimeouts.set(
|
||||
rtcSession.id,
|
||||
browser.setTimeout(() => {
|
||||
this.downloadTimeouts.delete(rtcSession.id);
|
||||
this.sfuClient?.updateDownload(rtcSession.id, {
|
||||
camera: false,
|
||||
screen: false,
|
||||
});
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!rtcSession.peerConnection) {
|
||||
return;
|
||||
}
|
||||
@@ -1586,6 +1831,22 @@ export const rtcService = {
|
||||
*/
|
||||
start(env, services) {
|
||||
const rtc = new Rtc(env, services);
|
||||
services["bus_service"].subscribe(
|
||||
"discuss.channel.rtc.session/sfu_hot_swap",
|
||||
async ({ serverInfo }) => {
|
||||
if (!rtc.state.selfSession) {
|
||||
return;
|
||||
}
|
||||
if (rtc.state.serverInfo?.url === serverInfo?.url) {
|
||||
// no reason to swap if the server is the same, if at some point we want to force a swap
|
||||
// there should be an explicit flag in the event payload.
|
||||
return;
|
||||
}
|
||||
rtc.state.serverInfo = serverInfo;
|
||||
await rtc.loadSfuClient();
|
||||
await rtc.call();
|
||||
}
|
||||
);
|
||||
services["bus_service"].subscribe(
|
||||
"discuss.channel.rtc.session/peer_notification",
|
||||
({ sender, notifications }) => {
|
||||
|
||||
@@ -94,6 +94,20 @@ export class RtcSession extends Record {
|
||||
return isActive && this.videoStreams.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{isSelfMuted: boolean, isDeaf: boolean, isTalking: boolean, isRaisingHand: boolean}}
|
||||
*/
|
||||
get info() {
|
||||
return {
|
||||
isSelfMuted: this.isSelfMuted,
|
||||
isRaisingHand: Boolean(this.raisingHand),
|
||||
isDeaf: this.isDeaf,
|
||||
isTalking: this.isTalking,
|
||||
isCameraOn: this.isCameraOn,
|
||||
isScreenSharingOn: this.isScreenSharingOn,
|
||||
};
|
||||
}
|
||||
|
||||
get partnerId() {
|
||||
const persona = this.channelMember?.persona;
|
||||
return persona.type === "partner" ? persona.id : undefined;
|
||||
|
||||
@@ -9,7 +9,7 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor {
|
||||
frequencies that we want to monitor in hz.
|
||||
* @param {number} [param0.processorOptions.minimumActiveCycles] - how many cycles have to pass since the last time the
|
||||
threshold was exceeded to go back to inactive state. It prevents the microphone to shut down
|
||||
when the user's voice drops in volume mid-sentence.
|
||||
when the user's voice drops in volume mid-sentence. Time in ms = minimumActiveCycles * processInterval.
|
||||
* @param {boolean} [param0.processorOptions.postAllTics] true if we need to postMessage at each tics, this prevents
|
||||
sending events to the main thread on all tics when not necessary.
|
||||
* @param {number} [param0.processorOptions.volumeThreshold] the minimum value for audio detection
|
||||
@@ -17,7 +17,7 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor {
|
||||
constructor({
|
||||
processorOptions: {
|
||||
frequencyRange,
|
||||
minimumActiveCycles = 10,
|
||||
minimumActiveCycles = 30,
|
||||
postAllTics,
|
||||
volumeThreshold = 0.3,
|
||||
},
|
||||
|
||||
@@ -80,6 +80,7 @@ class TestChannelRTC(MailCommon):
|
||||
('DELETE', [{'id': channel_member.rtc_session_ids.id - 1}]),
|
||||
],
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
'serverInfo': None,
|
||||
})
|
||||
|
||||
@users('employee')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
def get_twilio_credentials(env):
|
||||
def get_twilio_credentials(env) -> (str, str):
|
||||
"""
|
||||
To be overridable if we need to obtain credentials from another source.
|
||||
:return: tuple(account_sid: str, auth_token: str)
|
||||
@@ -10,3 +10,13 @@ def get_twilio_credentials(env):
|
||||
account_sid = params.get_param("mail.twilio_account_sid")
|
||||
auth_token = params.get_param("mail.twilio_account_token")
|
||||
return account_sid, auth_token
|
||||
|
||||
|
||||
def get_sfu_url(env) -> str | None:
|
||||
sfu_url = env['ir.config_parameter'].sudo().get_param("mail.sfu_server_url")
|
||||
if sfu_url:
|
||||
return sfu_url.rstrip("/")
|
||||
|
||||
|
||||
def get_sfu_key(env) -> str | None:
|
||||
return env['ir.config_parameter'].sudo().get_param('mail.sfu_server_key')
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import binascii
|
||||
import time
|
||||
import enum
|
||||
import hmac
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, utils
|
||||
|
||||
|
||||
class Algorithm(enum.Enum):
|
||||
ES256 = "ES256" # ECDSA SHA-256
|
||||
HS256 = "HS256" # HMAC SHA-256
|
||||
|
||||
|
||||
def _generate_keys(key_encoding, key_format) -> (bytes, bytes):
|
||||
private_object = ec.generate_private_key(ec.SECP256R1(), default_backend())
|
||||
private_int = private_object.private_numbers().private_value
|
||||
private_bytes = private_int.to_bytes(32, "big")
|
||||
public_object = private_object.public_key()
|
||||
public_bytes = public_object.public_bytes(
|
||||
encoding=key_encoding,
|
||||
format=key_format,
|
||||
)
|
||||
return private_bytes, public_bytes
|
||||
|
||||
|
||||
def generate_vapid_keys() -> (str, str):
|
||||
"""
|
||||
Generate the VAPID (Voluntary Application Server Identification) used for the Web Push
|
||||
This function generates a signing key pair usable with the Elliptic Curve Digital
|
||||
Signature Algorithm (ECDSA) over the P-256 curve.
|
||||
https://www.rfc-editor.org/rfc/rfc8292
|
||||
|
||||
:return: tuple (private_key, public_key)
|
||||
"""
|
||||
private, public = _generate_keys(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)
|
||||
private_string = base64.urlsafe_b64encode(private).decode("ascii").strip("=")
|
||||
public_string = base64.urlsafe_b64encode(public).decode("ascii").strip("=")
|
||||
return private_string, public_string
|
||||
|
||||
|
||||
def base64_decode_with_padding(value: str) -> bytes:
|
||||
return base64.urlsafe_b64decode(value + "==")
|
||||
|
||||
|
||||
def _generate_jwt(claims: dict, key: str, algorithm: Algorithm) -> str:
|
||||
JOSE_header = base64.urlsafe_b64encode(json.dumps({"typ": "JWT", "alg": algorithm.value}).encode())
|
||||
payload = base64.urlsafe_b64encode(json.dumps(claims).encode())
|
||||
unsigned_token = "{}.{}".format(JOSE_header.decode().strip("="), payload.decode().strip("="))
|
||||
key_decoded = base64_decode_with_padding(key)
|
||||
|
||||
match algorithm:
|
||||
case Algorithm.HS256:
|
||||
signature = hmac.new(key_decoded, unsigned_token.encode(), hashlib.sha256).digest()
|
||||
sig = base64.urlsafe_b64encode(signature)
|
||||
case Algorithm.ES256:
|
||||
# Retrieve the private key using a P256 elliptic curve
|
||||
private_key = ec.derive_private_key(
|
||||
int(binascii.hexlify(key_decoded), 16), ec.SECP256R1(), default_backend()
|
||||
)
|
||||
signature = private_key.sign(unsigned_token.encode(), ec.ECDSA(hashes.SHA256()))
|
||||
(r, s) = utils.decode_dss_signature(signature)
|
||||
sig = base64.urlsafe_b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big"))
|
||||
case _:
|
||||
raise ValueError(f"Unsupported algorithm: {algorithm}")
|
||||
|
||||
return "{}.{}".format(unsigned_token, sig.decode().strip("="))
|
||||
|
||||
|
||||
def sign(claims: dict, key: str, ttl: int, algorithm: Algorithm) -> str:
|
||||
"""
|
||||
A JSON Web Token is a signed pair of JSON objects, turned into base64 strings.
|
||||
|
||||
RFC: https://www.rfc-editor.org/rfc/rfc7519
|
||||
|
||||
:param claims: the payload of the jwt: https://www.rfc-editor.org/rfc/rfc7519#section-4.1
|
||||
:param key: base64 string
|
||||
:param ttl: the time to live of the token in seconds ('exp' claim)
|
||||
:param algorithm: to use to sign the token
|
||||
:return: JSON Web Token
|
||||
"""
|
||||
non_padded_key = key.strip("=")
|
||||
assert ttl
|
||||
claims["exp"] = int(time.time()) + ttl
|
||||
return _generate_jwt(claims, non_padded_key, algorithm=algorithm)
|
||||
@@ -66,6 +66,10 @@
|
||||
<field name="channel_type" groups="base.group_no_one"/>
|
||||
<field name="description" placeholder="Topics discussed in this group..."/>
|
||||
</group>
|
||||
<group class="o_label_nowrap" groups="base.group_no_one">
|
||||
<field name="sfu_channel_uuid"/>
|
||||
<field name="sfu_server_url"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Privacy" name="privacy">
|
||||
<group class="o_label_nowrap">
|
||||
|
||||
@@ -75,6 +75,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="SFU server">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="sfu_server_url" string="URL" class="col-lg-3"/>
|
||||
<field name="sfu_server_url"/>
|
||||
</div>
|
||||
<div class="row mt16">
|
||||
<label for="sfu_server_key" string="Key" class="col-lg-3"/>
|
||||
<field name="sfu_server_key"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Custom ICE server list" help="Configure your ICE server list for webRTC">
|
||||
<div class="content-group">
|
||||
<div class="row col-lg-4">
|
||||
|
||||
+22
-79
@@ -1,95 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import logging as logger
|
||||
import os
|
||||
import struct
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, utils
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .tools import jwt
|
||||
|
||||
MAX_PAYLOAD_SIZE = 4096
|
||||
|
||||
_logger = logger.getLogger(__name__)
|
||||
|
||||
def _base64_decode_with_padding(value):
|
||||
return base64.urlsafe_b64decode(value + '==')
|
||||
|
||||
def generate_web_push_vapid_key():
|
||||
"""
|
||||
Generate the VAPID (Voluntary Application Server Identification) used for the Web Push
|
||||
This function generates a signing key pair usable with the Elliptic Curve Digital
|
||||
Signature Algorithm (ECDSA) over the P-256 curve.
|
||||
These keys will be used during communication with the endpoint/browser
|
||||
https://www.rfc-editor.org/rfc/rfc8292
|
||||
"""
|
||||
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
|
||||
private_int = private_key.private_numbers().private_value
|
||||
private = private_int.to_bytes(32, 'big')
|
||||
private_string = base64.urlsafe_b64encode(private).decode('ascii').strip('=')
|
||||
|
||||
public_key = private_key.public_key()
|
||||
public = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.X962,
|
||||
format=serialization.PublicFormat.UncompressedPoint
|
||||
)
|
||||
public_string = base64.urlsafe_b64encode(public).decode('ascii').strip('=')
|
||||
return private_string, public_string
|
||||
|
||||
def _generate_jwt(endpoint, base_url, vapid_private_key):
|
||||
"""
|
||||
JWT are a pair of JSON objects, turned into base64 strings, and signed with the private ECDH key
|
||||
https://www.rfc-editor.org/rfc/rfc7519
|
||||
https://www.rfc-editor.org/rfc/rfc8291
|
||||
:param endpoint: the browser endpoint
|
||||
:param base_url: the base url
|
||||
:param vapid_private_key: the private ECDH key generate at mail_entreprise install
|
||||
:return:
|
||||
"""
|
||||
url = urlparse(endpoint)
|
||||
|
||||
jwt_info = base64.urlsafe_b64encode(json.dumps({
|
||||
'typ': 'JWT',
|
||||
'alg': 'ES256'
|
||||
}).encode())
|
||||
|
||||
# The expiration is a timestamp in seconds and must be no longer 12 hours.
|
||||
token_validity = 12 * 60 * 60
|
||||
|
||||
jwt_data = base64.urlsafe_b64encode(json.dumps({
|
||||
# aud: The “Audience” is a JWT construct that indicates the recipient scheme and host
|
||||
# e.g. for an endpoint like https://updates.push.services.mozilla.com/wpush/v2/gAAAAABY...,
|
||||
# the “aud” would be https://updates.push.services.mozilla.com
|
||||
'aud': '{}://{}'.format(url.scheme, url.netloc),
|
||||
# sub: the sub value needs to be either a URL address. This is so that if a push service needed to reach out
|
||||
# to sender, it can find contact information from the JWT.
|
||||
'sub': base_url,
|
||||
# exp: It's the expiration of the JWT, this prevents snoopers from being able to re-use a JWT if they intercept it.
|
||||
'exp': int(time.time()) + token_validity
|
||||
}).encode())
|
||||
|
||||
unsigned_token = '{}.{}'.format(jwt_info.decode().strip('='), jwt_data.decode().strip('='))
|
||||
|
||||
# Retrieve the private key using a P256 elliptic curve
|
||||
vapid_private_key_decoded = _base64_decode_with_padding(vapid_private_key)
|
||||
private_key = ec.derive_private_key(int(binascii.hexlify(vapid_private_key_decoded), 16), ec.SECP256R1(), default_backend())
|
||||
|
||||
# sign with ECDSA SHA-256
|
||||
signature = private_key.sign(unsigned_token.encode(), ec.ECDSA(hashes.SHA256()))
|
||||
(r, s) = utils.decode_dss_signature(signature)
|
||||
sig = base64.urlsafe_b64encode(r.to_bytes(32, 'big') + s.to_bytes(32, 'big'))
|
||||
|
||||
return '{}.{}'.format(unsigned_token, sig.decode().strip('='))
|
||||
|
||||
def _iv(base, counter):
|
||||
mask = int.from_bytes(base[4:], 'big')
|
||||
return base[:4] + (counter ^ mask).to_bytes(8, 'big')
|
||||
@@ -97,8 +27,8 @@ def _iv(base, counter):
|
||||
def _derive_key(salt, private_key, device):
|
||||
# browser keys
|
||||
device_keys = json.loads(device["keys"])
|
||||
p256dh = _base64_decode_with_padding(device_keys.get('p256dh'))
|
||||
auth = _base64_decode_with_padding(device_keys.get('auth'))
|
||||
p256dh = jwt.base64_decode_with_padding(device_keys.get('p256dh'))
|
||||
auth = jwt.base64_decode_with_padding(device_keys.get('auth'))
|
||||
|
||||
# generate a public key derived from the browser public key
|
||||
pub_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), p256dh)
|
||||
@@ -176,15 +106,28 @@ def _encrypt_payload(content, device, record_size=MAX_PAYLOAD_SIZE):
|
||||
return header + body
|
||||
|
||||
def push_to_end_point(base_url, device, payload, vapid_private_key, vapid_public_key, session):
|
||||
"""
|
||||
https://www.rfc-editor.org/rfc/rfc8291
|
||||
"""
|
||||
endpoint = device["endpoint"]
|
||||
jwt = _generate_jwt(endpoint, base_url, vapid_private_key)
|
||||
url = urlsplit(endpoint)
|
||||
jwt_claims = {
|
||||
# aud: The “Audience” is a JWT construct that indicates the recipient scheme and host
|
||||
# e.g. for an endpoint like https://updates.push.services.mozilla.com/wpush/v2/gAAAAABY...,
|
||||
# the “aud” would be https://updates.push.services.mozilla.com
|
||||
'aud': '{}://{}'.format(url.scheme, url.netloc),
|
||||
# sub: the sub value needs to be either a URL address. This is so that if a push service needed to reach out
|
||||
# to sender, it can find contact information from the JWT.
|
||||
'sub': base_url,
|
||||
}
|
||||
token = jwt.sign(jwt_claims, vapid_private_key, ttl=12 * 60 * 60, algorithm=jwt.Algorithm.ES256)
|
||||
body_payload = payload.encode()
|
||||
payload = _encrypt_payload(body_payload, device)
|
||||
headers = {
|
||||
# Authorization header field contains these parameters:
|
||||
# - "t" is the JWT;
|
||||
# - "k" the base64url-encoded key that signed that token.
|
||||
'Authorization': 'vapid t={}, k={}'.format(jwt, vapid_public_key),
|
||||
'Authorization': 'vapid t={}, k={}'.format(token, vapid_public_key),
|
||||
'Content-Encoding': 'aes128gcm',
|
||||
'TTL': '0',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user