diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 09e9b82a92a..4637bc0014b 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -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', ], diff --git a/addons/mail/i18n/mail.pot b/addons/mail/i18n/mail.pot index c1798333819..dd23db4e677 100644 --- a/addons/mail/i18n/mail.pot +++ b/addons/mail/i18n/mail.pot @@ -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 diff --git a/addons/mail/models/discuss/discuss_channel_member.py b/addons/mail/models/discuss/discuss_channel_member.py index 79dba4a10ed..0f943a665e7 100644 --- a/addons/mail/models/discuss/discuss_channel_member.py +++ b/addons/mail/models/discuss/discuss_channel_member.py @@ -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: diff --git a/addons/mail/models/discuss/discuss_channel_rtc_session.py b/addons/mail/models/discuss/discuss_channel_rtc_session.py index 9f31cdb9c4b..a97c5f2860e 100644 --- a/addons/mail/models/discuss/discuss_channel_rtc_session.py +++ b/addons/mail/models/discuss/discuss_channel_rtc_session.py @@ -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): diff --git a/addons/mail/models/partner_devices.py b/addons/mail/models/partner_devices.py index e739181289c..96be1aa2393 100644 --- a/addons/mail/models/partner_devices.py +++ b/addons/mail/models/partner_devices.py @@ -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") diff --git a/addons/mail/static/lib/odoo_sfu/odoo_sfu.js b/addons/mail/static/lib/odoo_sfu/odoo_sfu.js new file mode 100644 index 00000000000..7c8f8f9c628 --- /dev/null +++ b/addons/mail/static/lib/odoo_sfu/odoo_sfu.js @@ -0,0 +1,16331 @@ +/* @odoo-module */ +var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + +var lib$2 = {}; + +var browser = {exports: {}}; + +/** + * Helpers. + */ + +var ms; +var hasRequiredMs; + +function requireMs () { + if (hasRequiredMs) return ms; + hasRequiredMs = 1; + var s = 1000; + var m = s * 60; + var h = m * 60; + var d = h * 24; + var w = d * 7; + var y = d * 365.25; + + /** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + + ms = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isFinite(val)) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); + }; + + /** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + + function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'weeks': + case 'week': + case 'w': + return n * w; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } + } + + /** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function fmtShort(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return Math.round(ms / d) + 'd'; + } + if (msAbs >= h) { + return Math.round(ms / h) + 'h'; + } + if (msAbs >= m) { + return Math.round(ms / m) + 'm'; + } + if (msAbs >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; + } + + /** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function fmtLong(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return plural(ms, msAbs, d, 'day'); + } + if (msAbs >= h) { + return plural(ms, msAbs, h, 'hour'); + } + if (msAbs >= m) { + return plural(ms, msAbs, m, 'minute'); + } + if (msAbs >= s) { + return plural(ms, msAbs, s, 'second'); + } + return ms + ' ms'; + } + + /** + * Pluralization helper. + */ + + function plural(ms, msAbs, n, name) { + var isPlural = msAbs >= n * 1.5; + return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : ''); + } + return ms; +} + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + */ + +function setup(env) { + createDebug.debug = createDebug; + createDebug.default = createDebug; + createDebug.coerce = coerce; + createDebug.disable = disable; + createDebug.enable = enable; + createDebug.enabled = enabled; + createDebug.humanize = requireMs(); + createDebug.destroy = destroy; + + Object.keys(env).forEach(key => { + createDebug[key] = env[key]; + }); + + /** + * The currently active debug mode names, and names to skip. + */ + + createDebug.names = []; + createDebug.skips = []; + + /** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + createDebug.formatters = {}; + + /** + * Selects a color for a debug namespace + * @param {String} namespace The namespace string for the debug instance to be colored + * @return {Number|String} An ANSI color code for the given namespace + * @api private + */ + function selectColor(namespace) { + let hash = 0; + + for (let i = 0; i < namespace.length; i++) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; + } + createDebug.selectColor = selectColor; + + /** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + function createDebug(namespace) { + let prevTime; + let enableOverride = null; + let namespacesCache; + let enabledCache; + + function debug(...args) { + // Disabled? + if (!debug.enabled) { + return; + } + + const self = debug; + + // Set `diff` timestamp + const curr = Number(new Date()); + const ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + args[0] = createDebug.coerce(args[0]); + + if (typeof args[0] !== 'string') { + // Anything else let's inspect with %O + args.unshift('%O'); + } + + // Apply any `formatters` transformations + let index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { + // If we encounter an escaped % then don't increase the array index + if (match === '%%') { + return '%'; + } + index++; + const formatter = createDebug.formatters[format]; + if (typeof formatter === 'function') { + const val = args[index]; + match = formatter.call(self, val); + + // Now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // Apply env-specific formatting (colors, etc.) + createDebug.formatArgs.call(self, args); + + const logFn = self.log || createDebug.log; + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.useColors = createDebug.useColors(); + debug.color = createDebug.selectColor(namespace); + debug.extend = extend; + debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release. + + Object.defineProperty(debug, 'enabled', { + enumerable: true, + configurable: false, + get: () => { + if (enableOverride !== null) { + return enableOverride; + } + if (namespacesCache !== createDebug.namespaces) { + namespacesCache = createDebug.namespaces; + enabledCache = createDebug.enabled(namespace); + } + + return enabledCache; + }, + set: v => { + enableOverride = v; + } + }); + + // Env-specific initialization logic for debug instances + if (typeof createDebug.init === 'function') { + createDebug.init(debug); + } + + return debug; + } + + function extend(namespace, delimiter) { + const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace); + newDebug.log = this.log; + return newDebug; + } + + /** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + function enable(namespaces) { + createDebug.save(namespaces); + createDebug.namespaces = namespaces; + + createDebug.names = []; + createDebug.skips = []; + + let i; + const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + const len = split.length; + + for (i = 0; i < len; i++) { + if (!split[i]) { + // ignore empty strings + continue; + } + + namespaces = split[i].replace(/\*/g, '.*?'); + + if (namespaces[0] === '-') { + createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$')); + } else { + createDebug.names.push(new RegExp('^' + namespaces + '$')); + } + } + } + + /** + * Disable debug output. + * + * @return {String} namespaces + * @api public + */ + function disable() { + const namespaces = [ + ...createDebug.names.map(toNamespace), + ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace) + ].join(','); + createDebug.enable(''); + return namespaces; + } + + /** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + function enabled(name) { + if (name[name.length - 1] === '*') { + return true; + } + + let i; + let len; + + for (i = 0, len = createDebug.skips.length; i < len; i++) { + if (createDebug.skips[i].test(name)) { + return false; + } + } + + for (i = 0, len = createDebug.names.length; i < len; i++) { + if (createDebug.names[i].test(name)) { + return true; + } + } + + return false; + } + + /** + * Convert regexp to namespace + * + * @param {RegExp} regxep + * @return {String} namespace + * @api private + */ + function toNamespace(regexp) { + return regexp.toString() + .substring(2, regexp.toString().length - 2) + .replace(/\.\*\?$/, '*'); + } + + /** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + function coerce(val) { + if (val instanceof Error) { + return val.stack || val.message; + } + return val; + } + + /** + * XXX DO NOT USE. This is a temporary stub function. + * XXX It WILL be removed in the next major release. + */ + function destroy() { + console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); + } + + createDebug.enable(createDebug.load()); + + return createDebug; +} + +var common = setup; + +/* eslint-env browser */ +browser.exports; + +(function (module, exports) { + /** + * This is the web browser implementation of `debug()`. + */ + + exports.formatArgs = formatArgs; + exports.save = save; + exports.load = load; + exports.useColors = useColors; + exports.storage = localstorage(); + exports.destroy = (() => { + let warned = false; + + return () => { + if (!warned) { + warned = true; + console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); + } + }; + })(); + + /** + * Colors. + */ + + exports.colors = [ + '#0000CC', + '#0000FF', + '#0033CC', + '#0033FF', + '#0066CC', + '#0066FF', + '#0099CC', + '#0099FF', + '#00CC00', + '#00CC33', + '#00CC66', + '#00CC99', + '#00CCCC', + '#00CCFF', + '#3300CC', + '#3300FF', + '#3333CC', + '#3333FF', + '#3366CC', + '#3366FF', + '#3399CC', + '#3399FF', + '#33CC00', + '#33CC33', + '#33CC66', + '#33CC99', + '#33CCCC', + '#33CCFF', + '#6600CC', + '#6600FF', + '#6633CC', + '#6633FF', + '#66CC00', + '#66CC33', + '#9900CC', + '#9900FF', + '#9933CC', + '#9933FF', + '#99CC00', + '#99CC33', + '#CC0000', + '#CC0033', + '#CC0066', + '#CC0099', + '#CC00CC', + '#CC00FF', + '#CC3300', + '#CC3333', + '#CC3366', + '#CC3399', + '#CC33CC', + '#CC33FF', + '#CC6600', + '#CC6633', + '#CC9900', + '#CC9933', + '#CCCC00', + '#CCCC33', + '#FF0000', + '#FF0033', + '#FF0066', + '#FF0099', + '#FF00CC', + '#FF00FF', + '#FF3300', + '#FF3333', + '#FF3366', + '#FF3399', + '#FF33CC', + '#FF33FF', + '#FF6600', + '#FF6633', + '#FF9900', + '#FF9933', + '#FFCC00', + '#FFCC33' + ]; + + /** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + + // eslint-disable-next-line complexity + function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) { + return true; + } + + // Internet Explorer and Edge do not support colors. + if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { + return false; + } + + // Is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // Is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // Is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // Double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); + } + + /** + * Colorize log arguments if enabled. + * + * @api public + */ + + function formatArgs(args) { + args[0] = (this.useColors ? '%c' : '') + + this.namespace + + (this.useColors ? ' %c' : ' ') + + args[0] + + (this.useColors ? '%c ' : ' ') + + '+' + module.exports.humanize(this.diff); + + if (!this.useColors) { + return; + } + + const c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit'); + + // The final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + let index = 0; + let lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, match => { + if (match === '%%') { + return; + } + index++; + if (match === '%c') { + // We only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); + } + + /** + * Invokes `console.debug()` when available. + * No-op when `console.debug` is not a "function". + * If `console.debug` is not available, falls back + * to `console.log`. + * + * @api public + */ + exports.log = console.debug || console.log || (() => {}); + + /** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + function save(namespaces) { + try { + if (namespaces) { + exports.storage.setItem('debug', namespaces); + } else { + exports.storage.removeItem('debug'); + } + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + } + + /** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + function load() { + let r; + try { + r = exports.storage.getItem('debug'); + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; + } + + /** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + + function localstorage() { + try { + // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context + // The Browser also has localStorage in the global context. + return localStorage; + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + } + + module.exports = common(exports); + + const {formatters} = module.exports; + + /** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + + formatters.j = function (v) { + try { + return JSON.stringify(v); + } catch (error) { + return '[UnexpectedJSONParseError]: ' + error.message; + } + }; +} (browser, browser.exports)); + +var browserExports = browser.exports; + +var Device$1 = {}; + +var uaParser = {exports: {}}; + +uaParser.exports; + +(function (module, exports) { + ///////////////////////////////////////////////////////////////////////////////// + /* UAParser.js v1.0.37 + Copyright © 2012-2021 Faisal Salman + MIT License *//* + Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data. + Supports browser & node.js environment. + Demo : https://faisalman.github.io/ua-parser-js + Source : https://github.com/faisalman/ua-parser-js */ + ///////////////////////////////////////////////////////////////////////////////// + + (function (window, undefined$1) { + + ////////////// + // Constants + ///////////// + + + var LIBVERSION = '1.0.37', + EMPTY = '', + UNKNOWN = '?', + FUNC_TYPE = 'function', + UNDEF_TYPE = 'undefined', + OBJ_TYPE = 'object', + STR_TYPE = 'string', + MAJOR = 'major', + MODEL = 'model', + NAME = 'name', + TYPE = 'type', + VENDOR = 'vendor', + VERSION = 'version', + ARCHITECTURE= 'architecture', + CONSOLE = 'console', + MOBILE = 'mobile', + TABLET = 'tablet', + SMARTTV = 'smarttv', + WEARABLE = 'wearable', + EMBEDDED = 'embedded', + UA_MAX_LENGTH = 500; + + var AMAZON = 'Amazon', + APPLE = 'Apple', + ASUS = 'ASUS', + BLACKBERRY = 'BlackBerry', + BROWSER = 'Browser', + CHROME = 'Chrome', + EDGE = 'Edge', + FIREFOX = 'Firefox', + GOOGLE = 'Google', + HUAWEI = 'Huawei', + LG = 'LG', + MICROSOFT = 'Microsoft', + MOTOROLA = 'Motorola', + OPERA = 'Opera', + SAMSUNG = 'Samsung', + SHARP = 'Sharp', + SONY = 'Sony', + XIAOMI = 'Xiaomi', + ZEBRA = 'Zebra', + FACEBOOK = 'Facebook', + CHROMIUM_OS = 'Chromium OS', + MAC_OS = 'Mac OS'; + + /////////// + // Helper + ////////// + + var extend = function (regexes, extensions) { + var mergedRegexes = {}; + for (var i in regexes) { + if (extensions[i] && extensions[i].length % 2 === 0) { + mergedRegexes[i] = extensions[i].concat(regexes[i]); + } else { + mergedRegexes[i] = regexes[i]; + } + } + return mergedRegexes; + }, + enumerize = function (arr) { + var enums = {}; + for (var i=0; i 0) { + if (q.length === 2) { + if (typeof q[1] == FUNC_TYPE) { + // assign modified match + this[q[0]] = q[1].call(this, match); + } else { + // assign given value, ignore regex match + this[q[0]] = q[1]; + } + } else if (q.length === 3) { + // check whether function or regex + if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { + // call function (usually string mapper) + this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined$1; + } else { + // sanitize match using given regex + this[q[0]] = match ? match.replace(q[1], q[2]) : undefined$1; + } + } else if (q.length === 4) { + this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined$1; + } + } else { + this[q] = match ? match : undefined$1; + } + } + } + } + i += 2; + } + }, + + strMapper = function (str, map) { + + for (var i in map) { + // check if current value is array + if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { + for (var j = 0; j < map[i].length; j++) { + if (has(map[i][j], str)) { + return (i === UNKNOWN) ? undefined$1 : i; + } + } + } else if (has(map[i], str)) { + return (i === UNKNOWN) ? undefined$1 : i; + } + } + return str; + }; + + /////////////// + // String map + ////////////// + + // Safari < 3.0 + var oldSafariMap = { + '1.0' : '/8', + '1.2' : '/1', + '1.3' : '/3', + '2.0' : '/412', + '2.0.2' : '/416', + '2.0.3' : '/417', + '2.0.4' : '/419', + '?' : '/' + }, + windowsVersionMap = { + 'ME' : '4.90', + 'NT 3.11' : 'NT3.51', + 'NT 4.0' : 'NT4.0', + '2000' : 'NT 5.0', + 'XP' : ['NT 5.1', 'NT 5.2'], + 'Vista' : 'NT 6.0', + '7' : 'NT 6.1', + '8' : 'NT 6.2', + '8.1' : 'NT 6.3', + '10' : ['NT 6.4', 'NT 10.0'], + 'RT' : 'ARM' + }; + + ////////////// + // Regex map + ///////////// + + var regexes = { + + browser : [[ + + /\b(?:crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS + ], [VERSION, [NAME, 'Chrome']], [ + /edg(?:e|ios|a)?\/([\w\.]+)/i // Microsoft Edge + ], [VERSION, [NAME, 'Edge']], [ + + // Presto based + /(opera mini)\/([-\w\.]+)/i, // Opera Mini + /(opera [mobiletab]{3,6})\b.+version\/([-\w\.]+)/i, // Opera Mobi/Tablet + /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i // Opera + ], [NAME, VERSION], [ + /opios[\/ ]+([\w\.]+)/i // Opera mini on iphone >= 8.0 + ], [VERSION, [NAME, OPERA+' Mini']], [ + /\bopr\/([\w\.]+)/i // Opera Webkit + ], [VERSION, [NAME, OPERA]], [ + + // Mixed + /\bb[ai]*d(?:uhd|[ub]*[aekoprswx]{5,6})[\/ ]?([\w\.]+)/i // Baidu + ], [VERSION, [NAME, 'Baidu']], [ + /(kindle)\/([\w\.]+)/i, // Kindle + /(lunascape|maxthon|netfront|jasmine|blazer)[\/ ]?([\w\.]*)/i, // Lunascape/Maxthon/Netfront/Jasmine/Blazer + // Trident based + /(avant|iemobile|slim)\s?(?:browser)?[\/ ]?([\w\.]*)/i, // Avant/IEMobile/SlimBrowser + /(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer + + // Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon + /(flock|rockmelt|midori|epiphany|silk|skyfire|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|qq|duckduckgo)\/([-\w\.]+)/i, + // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ, aka ShouQ + /(heytap|ovi)browser\/([\d\.]+)/i, // Heytap/Ovi + /(weibo)__([\d\.]+)/i // Weibo + ], [NAME, VERSION], [ + /(?:\buc? ?browser|(?:juc.+)ucweb)[\/ ]?([\w\.]+)/i // UCBrowser + ], [VERSION, [NAME, 'UC'+BROWSER]], [ + /microm.+\bqbcore\/([\w\.]+)/i, // WeChat Desktop for Windows Built-in Browser + /\bqbcore\/([\w\.]+).+microm/i, + /micromessenger\/([\w\.]+)/i // WeChat + ], [VERSION, [NAME, 'WeChat']], [ + /konqueror\/([\w\.]+)/i // Konqueror + ], [VERSION, [NAME, 'Konqueror']], [ + /trident.+rv[: ]([\w\.]{1,9})\b.+like gecko/i // IE11 + ], [VERSION, [NAME, 'IE']], [ + /ya(?:search)?browser\/([\w\.]+)/i // Yandex + ], [VERSION, [NAME, 'Yandex']], [ + /slbrowser\/([\w\.]+)/i // Smart Lenovo Browser + ], [VERSION, [NAME, 'Smart Lenovo '+BROWSER]], [ + /(avast|avg)\/([\w\.]+)/i // Avast/AVG Secure Browser + ], [[NAME, /(.+)/, '$1 Secure '+BROWSER], VERSION], [ + /\bfocus\/([\w\.]+)/i // Firefox Focus + ], [VERSION, [NAME, FIREFOX+' Focus']], [ + /\bopt\/([\w\.]+)/i // Opera Touch + ], [VERSION, [NAME, OPERA+' Touch']], [ + /coc_coc\w+\/([\w\.]+)/i // Coc Coc Browser + ], [VERSION, [NAME, 'Coc Coc']], [ + /dolfin\/([\w\.]+)/i // Dolphin + ], [VERSION, [NAME, 'Dolphin']], [ + /coast\/([\w\.]+)/i // Opera Coast + ], [VERSION, [NAME, OPERA+' Coast']], [ + /miuibrowser\/([\w\.]+)/i // MIUI Browser + ], [VERSION, [NAME, 'MIUI '+BROWSER]], [ + /fxios\/([-\w\.]+)/i // Firefox for iOS + ], [VERSION, [NAME, FIREFOX]], [ + /\bqihu|(qi?ho?o?|360)browser/i // 360 + ], [[NAME, '360 ' + BROWSER]], [ + /(oculus|sailfish|huawei|vivo)browser\/([\w\.]+)/i + ], [[NAME, /(.+)/, '$1 ' + BROWSER], VERSION], [ // Oculus/Sailfish/HuaweiBrowser/VivoBrowser + /samsungbrowser\/([\w\.]+)/i // Samsung Internet + ], [VERSION, [NAME, SAMSUNG + ' Internet']], [ + /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon + ], [[NAME, /_/g, ' '], VERSION], [ + /metasr[\/ ]?([\d\.]+)/i // Sogou Explorer + ], [VERSION, [NAME, 'Sogou Explorer']], [ + /(sogou)mo\w+\/([\d\.]+)/i // Sogou Mobile + ], [[NAME, 'Sogou Mobile'], VERSION], [ + /(electron)\/([\w\.]+) safari/i, // Electron-based App + /(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i, // Tesla + /m?(qqbrowser|2345Explorer)[\/ ]?([\w\.]+)/i // QQBrowser/2345 Browser + ], [NAME, VERSION], [ + /(lbbrowser)/i, // LieBao Browser + /\[(linkedin)app\]/i // LinkedIn App for iOS & Android + ], [NAME], [ + + // WebView + /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i // Facebook App for iOS & Android + ], [[NAME, FACEBOOK], VERSION], [ + /(Klarna)\/([\w\.]+)/i, // Klarna Shopping Browser for iOS & Android + /(kakao(?:talk|story))[\/ ]([\w\.]+)/i, // Kakao App + /(naver)\(.*?(\d+\.[\w\.]+).*\)/i, // Naver InApp + /safari (line)\/([\w\.]+)/i, // Line App for iOS + /\b(line)\/([\w\.]+)\/iab/i, // Line App for Android + /(alipay)client\/([\w\.]+)/i, // Alipay + /(chromium|instagram|snapchat)[\/ ]([-\w\.]+)/i // Chromium/Instagram/Snapchat + ], [NAME, VERSION], [ + /\bgsa\/([\w\.]+) .*safari\//i // Google Search Appliance on iOS + ], [VERSION, [NAME, 'GSA']], [ + /musical_ly(?:.+app_?version\/|_)([\w\.]+)/i // TikTok + ], [VERSION, [NAME, 'TikTok']], [ + + /headlesschrome(?:\/([\w\.]+)| )/i // Chrome Headless + ], [VERSION, [NAME, CHROME+' Headless']], [ + + / wv\).+(chrome)\/([\w\.]+)/i // Chrome WebView + ], [[NAME, CHROME+' WebView'], VERSION], [ + + /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i // Android Browser + ], [VERSION, [NAME, 'Android '+BROWSER]], [ + + /(chrome|omniweb|arora|[tizenoka]{5} ?browser)\/v?([\w\.]+)/i // Chrome/OmniWeb/Arora/Tizen/Nokia + ], [NAME, VERSION], [ + + /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i // Mobile Safari + ], [VERSION, [NAME, 'Mobile Safari']], [ + /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i // Safari & Safari Mobile + ], [VERSION, NAME], [ + /webkit.+?(mobile ?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 + ], [NAME, [VERSION, strMapper, oldSafariMap]], [ + + /(webkit|khtml)\/([\w\.]+)/i + ], [NAME, VERSION], [ + + // Gecko based + /(navigator|netscape\d?)\/([-\w\.]+)/i // Netscape + ], [[NAME, 'Netscape'], VERSION], [ + /mobile vr; rv:([\w\.]+)\).+firefox/i // Firefox Reality + ], [VERSION, [NAME, FIREFOX+' Reality']], [ + /ekiohf.+(flow)\/([\w\.]+)/i, // Flow + /(swiftfox)/i, // Swiftfox + /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[\/ ]?([\w\.\+]+)/i, + // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror/Klar + /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, + // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix + /(firefox)\/([\w\.]+)/i, // Other Firefox-based + /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla + + // Other + /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, + // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir/Obigo/Mosaic/Go/ICE/UP.Browser + /(links) \(([\w\.]+)/i, // Links + /panasonic;(viera)/i // Panasonic Viera + ], [NAME, VERSION], [ + + /(cobalt)\/([\w\.]+)/i // Cobalt + ], [NAME, [VERSION, /master.|lts./, ""]] + ], + + cpu : [[ + + /(?:(amd|x(?:(?:86|64)[-_])?|wow|win)64)[;\)]/i // AMD64 (x64) + ], [[ARCHITECTURE, 'amd64']], [ + + /(ia32(?=;))/i // IA32 (quicktime) + ], [[ARCHITECTURE, lowerize]], [ + + /((?:i[346]|x)86)[;\)]/i // IA32 (x86) + ], [[ARCHITECTURE, 'ia32']], [ + + /\b(aarch64|arm(v?8e?l?|_?64))\b/i // ARM64 + ], [[ARCHITECTURE, 'arm64']], [ + + /\b(arm(?:v[67])?ht?n?[fl]p?)\b/i // ARMHF + ], [[ARCHITECTURE, 'armhf']], [ + + // PocketPC mistakenly identified as PowerPC + /windows (ce|mobile); ppc;/i + ], [[ARCHITECTURE, 'arm']], [ + + /((?:ppc|powerpc)(?:64)?)(?: mac|;|\))/i // PowerPC + ], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [ + + /(sun4\w)[;\)]/i // SPARC + ], [[ARCHITECTURE, 'sparc']], [ + + /((?:avr32|ia64(?=;))|68k(?=\))|\barm(?=v(?:[1-7]|[5-7]1)l?|;|eabi)|(?=atmel )avr|(?:irix|mips|sparc)(?:64)?\b|pa-risc)/i + // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC + ], [[ARCHITECTURE, lowerize]] + ], + + device : [[ + + ////////////////////////// + // MOBILES & TABLETS + ///////////////////////// + + // Samsung + /\b(sch-i[89]0\d|shw-m380s|sm-[ptx]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i + ], [MODEL, [VENDOR, SAMSUNG], [TYPE, TABLET]], [ + /\b((?:s[cgp]h|gt|sm)-\w+|sc[g-]?[\d]+a?|galaxy nexus)/i, + /samsung[- ]([-\w]+)/i, + /sec-(sgh\w+)/i + ], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [ + + // Apple + /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone + ], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [ + /\((ipad);[-\w\),; ]+apple/i, // iPad + /applecoremedia\/[\w\.]+ \((ipad)/i, + /\b(ipad)\d\d?,\d\d?[;\]].+ios/i + ], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [ + /(macintosh);/i + ], [MODEL, [VENDOR, APPLE]], [ + + // Sharp + /\b(sh-?[altvz]?\d\d[a-ekm]?)/i + ], [MODEL, [VENDOR, SHARP], [TYPE, MOBILE]], [ + + // Huawei + /\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i + ], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [ + /(?:huawei|honor)([-\w ]+)[;\)]/i, + /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i + ], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [ + + // Xiaomi + /\b(poco[\w ]+|m2\d{3}j\d\d[a-z]{2})(?: bui|\))/i, // Xiaomi POCO + /\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models + /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi + /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi + /oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models + /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i // Xiaomi Mi + ], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [ + /oid[^\)]+; (2\d{4}(283|rpbf)[cgl])( bui|\))/i, // Redmi Pad + /\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i // Mi Pad tablets + ],[[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, TABLET]], [ + + // OPPO + /; (\w+) bui.+ oppo/i, + /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i + ], [MODEL, [VENDOR, 'OPPO'], [TYPE, MOBILE]], [ + + // Vivo + /vivo (\w+)(?: bui|\))/i, + /\b(v[12]\d{3}\w?[at])(?: bui|;)/i + ], [MODEL, [VENDOR, 'Vivo'], [TYPE, MOBILE]], [ + + // Realme + /\b(rmx[1-3]\d{3})(?: bui|;|\))/i + ], [MODEL, [VENDOR, 'Realme'], [TYPE, MOBILE]], [ + + // Motorola + /\b(milestone|droid(?:[2-4x]| (?:bionic|x2|pro|razr))?:?( 4g)?)\b[\w ]+build\//i, + /\bmot(?:orola)?[- ](\w*)/i, + /((?:moto[\w\(\) ]+|xt\d{3,4}|nexus 6)(?= bui|\)))/i + ], [MODEL, [VENDOR, MOTOROLA], [TYPE, MOBILE]], [ + /\b(mz60\d|xoom[2 ]{0,2}) build\//i + ], [MODEL, [VENDOR, MOTOROLA], [TYPE, TABLET]], [ + + // LG + /((?=lg)?[vl]k\-?\d{3}) bui| 3\.[-\w; ]{10}lg?-([06cv9]{3,4})/i + ], [MODEL, [VENDOR, LG], [TYPE, TABLET]], [ + /(lm(?:-?f100[nv]?|-[\w\.]+)(?= bui|\))|nexus [45])/i, + /\blg[-e;\/ ]+((?!browser|netcast|android tv)\w+)/i, + /\blg-?([\d\w]+) bui/i + ], [MODEL, [VENDOR, LG], [TYPE, MOBILE]], [ + + // Lenovo + /(ideatab[-\w ]+)/i, + /lenovo ?(s[56]000[-\w]+|tab(?:[\w ]+)|yt[-\d\w]{6}|tb[-\d\w]{6})/i + ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [ + + // Nokia + /(?:maemo|nokia).*(n900|lumia \d+)/i, + /nokia[-_ ]?([-\w\.]*)/i + ], [[MODEL, /_/g, ' '], [VENDOR, 'Nokia'], [TYPE, MOBILE]], [ + + // Google + /(pixel c)\b/i // Google Pixel C + ], [MODEL, [VENDOR, GOOGLE], [TYPE, TABLET]], [ + /droid.+; (pixel[\daxl ]{0,6})(?: bui|\))/i // Google Pixel + ], [MODEL, [VENDOR, GOOGLE], [TYPE, MOBILE]], [ + + // Sony + /droid.+ (a?\d[0-2]{2}so|[c-g]\d{4}|so[-gl]\w+|xq-a\w[4-7][12])(?= bui|\).+chrome\/(?![1-6]{0,1}\d\.))/i + ], [MODEL, [VENDOR, SONY], [TYPE, MOBILE]], [ + /sony tablet [ps]/i, + /\b(?:sony)?sgp\w+(?: bui|\))/i + ], [[MODEL, 'Xperia Tablet'], [VENDOR, SONY], [TYPE, TABLET]], [ + + // OnePlus + / (kb2005|in20[12]5|be20[12][59])\b/i, + /(?:one)?(?:plus)? (a\d0\d\d)(?: b|\))/i + ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [ + + // Amazon + /(alexa)webm/i, + /(kf[a-z]{2}wi|aeo[c-r]{2})( bui|\))/i, // Kindle Fire without Silk / Echo Show + /(kf[a-z]+)( bui|\)).+silk\//i // Kindle Fire HD + ], [MODEL, [VENDOR, AMAZON], [TYPE, TABLET]], [ + /((?:sd|kf)[0349hijorstuw]+)( bui|\)).+silk\//i // Fire Phone + ], [[MODEL, /(.+)/g, 'Fire Phone $1'], [VENDOR, AMAZON], [TYPE, MOBILE]], [ + + // BlackBerry + /(playbook);[-\w\),; ]+(rim)/i // BlackBerry PlayBook + ], [MODEL, VENDOR, [TYPE, TABLET]], [ + /\b((?:bb[a-f]|st[hv])100-\d)/i, + /\(bb10; (\w+)/i // BlackBerry 10 + ], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [ + + // Asus + /(?:\b|asus_)(transfo[prime ]{4,10} \w+|eeepc|slider \w+|nexus 7|padfone|p00[cj])/i + ], [MODEL, [VENDOR, ASUS], [TYPE, TABLET]], [ + / (z[bes]6[027][012][km][ls]|zenfone \d\w?)\b/i + ], [MODEL, [VENDOR, ASUS], [TYPE, MOBILE]], [ + + // HTC + /(nexus 9)/i // HTC Nexus 9 + ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [ + /(htc)[-;_ ]{1,2}([\w ]+(?=\)| bui)|\w+)/i, // HTC + + // ZTE + /(zte)[- ]([\w ]+?)(?: bui|\/|\))/i, + /(alcatel|geeksphone|nexian|panasonic(?!(?:;|\.))|sony(?!-bra))[-_ ]?([-\w]*)/i // Alcatel/GeeksPhone/Nexian/Panasonic/Sony + ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [ + + // Acer + /droid.+; ([ab][1-7]-?[0178a]\d\d?)/i + ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [ + + // Meizu + /droid.+; (m[1-5] note) bui/i, + /\bmz-([-\w]{2,})/i + ], [MODEL, [VENDOR, 'Meizu'], [TYPE, MOBILE]], [ + + // Ulefone + /; ((?:power )?armor(?:[\w ]{0,8}))(?: bui|\))/i + ], [MODEL, [VENDOR, 'Ulefone'], [TYPE, MOBILE]], [ + + // MIXED + /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron|infinix|tecno)[-_ ]?([-\w]*)/i, + // BlackBerry/BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron + /(hp) ([\w ]+\w)/i, // HP iPAQ + /(asus)-?(\w+)/i, // Asus + /(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia + /(lenovo)[-_ ]?([-\w]+)/i, // Lenovo + /(jolla)/i, // Jolla + /(oppo) ?([\w ]+) bui/i // OPPO + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + + /(kobo)\s(ereader|touch)/i, // Kobo + /(archos) (gamepad2?)/i, // Archos + /(hp).+(touchpad(?!.+tablet)|tablet)/i, // HP TouchPad + /(kindle)\/([\w\.]+)/i, // Kindle + /(nook)[\w ]+build\/(\w+)/i, // Nook + /(dell) (strea[kpr\d ]*[\dko])/i, // Dell Streak + /(le[- ]+pan)[- ]+(\w{1,9}) bui/i, // Le Pan Tablets + /(trinity)[- ]*(t\d{3}) bui/i, // Trinity Tablets + /(gigaset)[- ]+(q\w{1,9}) bui/i, // Gigaset Tablets + /(vodafone) ([\w ]+)(?:\)| bui)/i // Vodafone + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /(surface duo)/i // Surface Duo + ], [MODEL, [VENDOR, MICROSOFT], [TYPE, TABLET]], [ + /droid [\d\.]+; (fp\du?)(?: b|\))/i // Fairphone + ], [MODEL, [VENDOR, 'Fairphone'], [TYPE, MOBILE]], [ + /(u304aa)/i // AT&T + ], [MODEL, [VENDOR, 'AT&T'], [TYPE, MOBILE]], [ + /\bsie-(\w*)/i // Siemens + ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [ + /\b(rct\w+) b/i // RCA Tablets + ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [ + /\b(venue[\d ]{2,7}) b/i // Dell Venue Tablets + ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [ + /\b(q(?:mv|ta)\w+) b/i // Verizon Tablet + ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [ + /\b(?:barnes[& ]+noble |bn[rt])([\w\+ ]*) b/i // Barnes & Noble Tablet + ], [MODEL, [VENDOR, 'Barnes & Noble'], [TYPE, TABLET]], [ + /\b(tm\d{3}\w+) b/i + ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [ + /\b(k88) b/i // ZTE K Series Tablet + ], [MODEL, [VENDOR, 'ZTE'], [TYPE, TABLET]], [ + /\b(nx\d{3}j) b/i // ZTE Nubia + ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [ + /\b(gen\d{3}) b.+49h/i // Swiss GEN Mobile + ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [ + /\b(zur\d{3}) b/i // Swiss ZUR Tablet + ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [ + /\b((zeki)?tb.*\b) b/i // Zeki Tablets + ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [ + /\b([yr]\d{2}) b/i, + /\b(dragon[- ]+touch |dt)(\w{5}) b/i // Dragon Touch Tablet + ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [ + /\b(ns-?\w{0,9}) b/i // Insignia Tablets + ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [ + /\b((nxa|next)-?\w{0,9}) b/i // NextBook Tablets + ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [ + /\b(xtreme\_)?(v(1[045]|2[015]|[3469]0|7[05])) b/i // Voice Xtreme Phones + ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [ + /\b(lvtel\-)?(v1[12]) b/i // LvTel Phones + ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [ + /\b(ph-1) /i // Essential PH-1 + ], [MODEL, [VENDOR, 'Essential'], [TYPE, MOBILE]], [ + /\b(v(100md|700na|7011|917g).*\b) b/i // Envizen Tablets + ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [ + /\b(trio[-\w\. ]+) b/i // MachSpeed Tablets + ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [ + /\btu_(1491) b/i // Rotor Tablets + ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [ + /(shield[\w ]+) b/i // Nvidia Shield Tablets + ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, TABLET]], [ + /(sprint) (\w+)/i // Sprint Phones + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + /(kin\.[onetw]{3})/i // Microsoft Kin + ], [[MODEL, /\./g, ' '], [VENDOR, MICROSOFT], [TYPE, MOBILE]], [ + /droid.+; (cc6666?|et5[16]|mc[239][23]x?|vc8[03]x?)\)/i // Zebra + ], [MODEL, [VENDOR, ZEBRA], [TYPE, TABLET]], [ + /droid.+; (ec30|ps20|tc[2-8]\d[kx])\)/i + ], [MODEL, [VENDOR, ZEBRA], [TYPE, MOBILE]], [ + + /////////////////// + // SMARTTVS + /////////////////// + + /smart-tv.+(samsung)/i // Samsung + ], [VENDOR, [TYPE, SMARTTV]], [ + /hbbtv.+maple;(\d+)/i + ], [[MODEL, /^/, 'SmartTV'], [VENDOR, SAMSUNG], [TYPE, SMARTTV]], [ + /(nux; netcast.+smarttv|lg (netcast\.tv-201\d|android tv))/i // LG SmartTV + ], [[VENDOR, LG], [TYPE, SMARTTV]], [ + /(apple) ?tv/i // Apple TV + ], [VENDOR, [MODEL, APPLE+' TV'], [TYPE, SMARTTV]], [ + /crkey/i // Google Chromecast + ], [[MODEL, CHROME+'cast'], [VENDOR, GOOGLE], [TYPE, SMARTTV]], [ + /droid.+aft(\w+)( bui|\))/i // Fire TV + ], [MODEL, [VENDOR, AMAZON], [TYPE, SMARTTV]], [ + /\(dtv[\);].+(aquos)/i, + /(aquos-tv[\w ]+)\)/i // Sharp + ], [MODEL, [VENDOR, SHARP], [TYPE, SMARTTV]],[ + /(bravia[\w ]+)( bui|\))/i // Sony + ], [MODEL, [VENDOR, SONY], [TYPE, SMARTTV]], [ + /(mitv-\w{5}) bui/i // Xiaomi + ], [MODEL, [VENDOR, XIAOMI], [TYPE, SMARTTV]], [ + /Hbbtv.*(technisat) (.*);/i // TechniSAT + ], [VENDOR, MODEL, [TYPE, SMARTTV]], [ + /\b(roku)[\dx]*[\)\/]((?:dvp-)?[\d\.]*)/i, // Roku + /hbbtv\/\d+\.\d+\.\d+ +\([\w\+ ]*; *([\w\d][^;]*);([^;]*)/i // HbbTV devices + ], [[VENDOR, trim], [MODEL, trim], [TYPE, SMARTTV]], [ + /\b(android tv|smart[- ]?tv|opera tv|tv; rv:)\b/i // SmartTV from Unidentified Vendors + ], [[TYPE, SMARTTV]], [ + + /////////////////// + // CONSOLES + /////////////////// + + /(ouya)/i, // Ouya + /(nintendo) ([wids3utch]+)/i // Nintendo + ], [VENDOR, MODEL, [TYPE, CONSOLE]], [ + /droid.+; (shield) bui/i // Nvidia + ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [ + /(playstation [345portablevi]+)/i // Playstation + ], [MODEL, [VENDOR, SONY], [TYPE, CONSOLE]], [ + /\b(xbox(?: one)?(?!; xbox))[\); ]/i // Microsoft Xbox + ], [MODEL, [VENDOR, MICROSOFT], [TYPE, CONSOLE]], [ + + /////////////////// + // WEARABLES + /////////////////// + + /((pebble))app/i // Pebble + ], [VENDOR, MODEL, [TYPE, WEARABLE]], [ + /(watch)(?: ?os[,\/]|\d,\d\/)[\d\.]+/i // Apple Watch + ], [MODEL, [VENDOR, APPLE], [TYPE, WEARABLE]], [ + /droid.+; (glass) \d/i // Google Glass + ], [MODEL, [VENDOR, GOOGLE], [TYPE, WEARABLE]], [ + /droid.+; (wt63?0{2,3})\)/i + ], [MODEL, [VENDOR, ZEBRA], [TYPE, WEARABLE]], [ + /(quest( 2| pro)?)/i // Oculus Quest + ], [MODEL, [VENDOR, FACEBOOK], [TYPE, WEARABLE]], [ + + /////////////////// + // EMBEDDED + /////////////////// + + /(tesla)(?: qtcarbrowser|\/[-\w\.]+)/i // Tesla + ], [VENDOR, [TYPE, EMBEDDED]], [ + /(aeobc)\b/i // Echo Dot + ], [MODEL, [VENDOR, AMAZON], [TYPE, EMBEDDED]], [ + + //////////////////// + // MIXED (GENERIC) + /////////////////// + + /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew).+? mobile safari/i // Android Phones from Unidentified Vendors + ], [MODEL, [TYPE, MOBILE]], [ + /droid .+?; ([^;]+?)(?: bui|\) applew).+?(?! mobile) safari/i // Android Tablets from Unidentified Vendors + ], [MODEL, [TYPE, TABLET]], [ + /\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i // Unidentifiable Tablet + ], [[TYPE, TABLET]], [ + /(phone|mobile(?:[;\/]| [ \w\/\.]*safari)|pda(?=.+windows ce))/i // Unidentifiable Mobile + ], [[TYPE, MOBILE]], [ + /(android[-\w\. ]{0,9});.+buil/i // Generic Android Device + ], [MODEL, [VENDOR, 'Generic']] + ], + + engine : [[ + + /windows.+ edge\/([\w\.]+)/i // EdgeHTML + ], [VERSION, [NAME, EDGE+'HTML']], [ + + /webkit\/537\.36.+chrome\/(?!27)([\w\.]+)/i // Blink + ], [VERSION, [NAME, 'Blink']], [ + + /(presto)\/([\w\.]+)/i, // Presto + /(webkit|trident|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m/Goanna + /ekioh(flow)\/([\w\.]+)/i, // Flow + /(khtml|tasman|links)[\/ ]\(?([\w\.]+)/i, // KHTML/Tasman/Links + /(icab)[\/ ]([23]\.[\d\.]+)/i, // iCab + /\b(libweb)/i + ], [NAME, VERSION], [ + + /rv\:([\w\.]{1,9})\b.+(gecko)/i // Gecko + ], [VERSION, NAME] + ], + + os : [[ + + // Windows + /microsoft (windows) (vista|xp)/i // Windows (iTunes) + ], [NAME, VERSION], [ + /(windows (?:phone(?: os)?|mobile))[\/ ]?([\d\.\w ]*)/i // Windows Phone + ], [NAME, [VERSION, strMapper, windowsVersionMap]], [ + /windows nt 6\.2; (arm)/i, // Windows RT + /windows[\/ ]?([ntce\d\. ]+\w)(?!.+xbox)/i, + /(?:win(?=3|9|n)|win 9x )([nt\d\.]+)/i + ], [[VERSION, strMapper, windowsVersionMap], [NAME, 'Windows']], [ + + // iOS/macOS + /ip[honead]{2,4}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS + /(?:ios;fbsv\/|iphone.+ios[\/ ])([\d\.]+)/i, + /cfnetwork\/.+darwin/i + ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [ + /(mac os x) ?([\w\. ]*)/i, + /(macintosh|mac_powerpc\b)(?!.+haiku)/i // Mac OS + ], [[NAME, MAC_OS], [VERSION, /_/g, '.']], [ + + // Mobile OSes + /droid ([\w\.]+)\b.+(android[- ]x86|harmonyos)/i // Android-x86/HarmonyOS + ], [VERSION, NAME], [ // Android/WebOS/QNX/Bada/RIM/Maemo/MeeGo/Sailfish OS + /(android|webos|qnx|bada|rim tablet os|maemo|meego|sailfish)[-\/ ]?([\w\.]*)/i, + /(blackberry)\w*\/([\w\.]*)/i, // Blackberry + /(tizen|kaios)[\/ ]([\w\.]+)/i, // Tizen/KaiOS + /\((series40);/i // Series 40 + ], [NAME, VERSION], [ + /\(bb(10);/i // BlackBerry 10 + ], [VERSION, [NAME, BLACKBERRY]], [ + /(?:symbian ?os|symbos|s60(?=;)|series60)[-\/ ]?([\w\.]*)/i // Symbian + ], [VERSION, [NAME, 'Symbian']], [ + /mozilla\/[\d\.]+ \((?:mobile|tablet|tv|mobile; [\w ]+); rv:.+ gecko\/([\w\.]+)/i // Firefox OS + ], [VERSION, [NAME, FIREFOX+' OS']], [ + /web0s;.+rt(tv)/i, + /\b(?:hp)?wos(?:browser)?\/([\w\.]+)/i // WebOS + ], [VERSION, [NAME, 'webOS']], [ + /watch(?: ?os[,\/]|\d,\d\/)([\d\.]+)/i // watchOS + ], [VERSION, [NAME, 'watchOS']], [ + + // Google Chromecast + /crkey\/([\d\.]+)/i // Google Chromecast + ], [VERSION, [NAME, CHROME+'cast']], [ + /(cros) [\w]+(?:\)| ([\w\.]+)\b)/i // Chromium OS + ], [[NAME, CHROMIUM_OS], VERSION],[ + + // Smart TVs + /panasonic;(viera)/i, // Panasonic Viera + /(netrange)mmh/i, // Netrange + /(nettv)\/(\d+\.[\w\.]+)/i, // NetTV + + // Console + /(nintendo|playstation) ([wids345portablevuch]+)/i, // Nintendo/Playstation + /(xbox); +xbox ([^\);]+)/i, // Microsoft Xbox (360, One, X, S, Series X, Series S) + + // Other + /\b(joli|palm)\b ?(?:os)?\/?([\w\.]*)/i, // Joli/Palm + /(mint)[\/\(\) ]?(\w*)/i, // Mint + /(mageia|vectorlinux)[; ]/i, // Mageia/VectorLinux + /([kxln]?ubuntu|debian|suse|opensuse|gentoo|arch(?= linux)|slackware|fedora|mandriva|centos|pclinuxos|red ?hat|zenwalk|linpus|raspbian|plan 9|minix|risc os|contiki|deepin|manjaro|elementary os|sabayon|linspire)(?: gnu\/linux)?(?: enterprise)?(?:[- ]linux)?(?:-gnu)?[-\/ ]?(?!chrom|package)([-\w\.]*)/i, + // Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware/Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus/Raspbian/Plan9/Minix/RISCOS/Contiki/Deepin/Manjaro/elementary/Sabayon/Linspire + /(hurd|linux) ?([\w\.]*)/i, // Hurd/Linux + /(gnu) ?([\w\.]*)/i, // GNU + /\b([-frentopcghs]{0,5}bsd|dragonfly)[\/ ]?(?!amd|[ix346]{1,2}86)([\w\.]*)/i, // FreeBSD/NetBSD/OpenBSD/PC-BSD/GhostBSD/DragonFly + /(haiku) (\w+)/i // Haiku + ], [NAME, VERSION], [ + /(sunos) ?([\w\.\d]*)/i // Solaris + ], [[NAME, 'Solaris'], VERSION], [ + /((?:open)?solaris)[-\/ ]?([\w\.]*)/i, // Solaris + /(aix) ((\d)(?=\.|\)| )[\w\.])*/i, // AIX + /\b(beos|os\/2|amigaos|morphos|openvms|fuchsia|hp-ux|serenityos)/i, // BeOS/OS2/AmigaOS/MorphOS/OpenVMS/Fuchsia/HP-UX/SerenityOS + /(unix) ?([\w\.]*)/i // UNIX + ], [NAME, VERSION] + ] + }; + + ///////////////// + // Constructor + //////////////// + + var UAParser = function (ua, extensions) { + + if (typeof ua === OBJ_TYPE) { + extensions = ua; + ua = undefined$1; + } + + if (!(this instanceof UAParser)) { + return new UAParser(ua, extensions).getResult(); + } + + var _navigator = (typeof window !== UNDEF_TYPE && window.navigator) ? window.navigator : undefined$1; + var _ua = ua || ((_navigator && _navigator.userAgent) ? _navigator.userAgent : EMPTY); + var _uach = (_navigator && _navigator.userAgentData) ? _navigator.userAgentData : undefined$1; + var _rgxmap = extensions ? extend(regexes, extensions) : regexes; + var _isSelfNav = _navigator && _navigator.userAgent == _ua; + + this.getBrowser = function () { + var _browser = {}; + _browser[NAME] = undefined$1; + _browser[VERSION] = undefined$1; + rgxMapper.call(_browser, _ua, _rgxmap.browser); + _browser[MAJOR] = majorize(_browser[VERSION]); + // Brave-specific detection + if (_isSelfNav && _navigator && _navigator.brave && typeof _navigator.brave.isBrave == FUNC_TYPE) { + _browser[NAME] = 'Brave'; + } + return _browser; + }; + this.getCPU = function () { + var _cpu = {}; + _cpu[ARCHITECTURE] = undefined$1; + rgxMapper.call(_cpu, _ua, _rgxmap.cpu); + return _cpu; + }; + this.getDevice = function () { + var _device = {}; + _device[VENDOR] = undefined$1; + _device[MODEL] = undefined$1; + _device[TYPE] = undefined$1; + rgxMapper.call(_device, _ua, _rgxmap.device); + if (_isSelfNav && !_device[TYPE] && _uach && _uach.mobile) { + _device[TYPE] = MOBILE; + } + // iPadOS-specific detection: identified as Mac, but has some iOS-only properties + if (_isSelfNav && _device[MODEL] == 'Macintosh' && _navigator && typeof _navigator.standalone !== UNDEF_TYPE && _navigator.maxTouchPoints && _navigator.maxTouchPoints > 2) { + _device[MODEL] = 'iPad'; + _device[TYPE] = TABLET; + } + return _device; + }; + this.getEngine = function () { + var _engine = {}; + _engine[NAME] = undefined$1; + _engine[VERSION] = undefined$1; + rgxMapper.call(_engine, _ua, _rgxmap.engine); + return _engine; + }; + this.getOS = function () { + var _os = {}; + _os[NAME] = undefined$1; + _os[VERSION] = undefined$1; + rgxMapper.call(_os, _ua, _rgxmap.os); + if (_isSelfNav && !_os[NAME] && _uach && _uach.platform != 'Unknown') { + _os[NAME] = _uach.platform + .replace(/chrome os/i, CHROMIUM_OS) + .replace(/macos/i, MAC_OS); // backward compatibility + } + return _os; + }; + this.getResult = function () { + return { + ua : this.getUA(), + browser : this.getBrowser(), + engine : this.getEngine(), + os : this.getOS(), + device : this.getDevice(), + cpu : this.getCPU() + }; + }; + this.getUA = function () { + return _ua; + }; + this.setUA = function (ua) { + _ua = (typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH) ? trim(ua, UA_MAX_LENGTH) : ua; + return this; + }; + this.setUA(_ua); + return this; + }; + + UAParser.VERSION = LIBVERSION; + UAParser.BROWSER = enumerize([NAME, VERSION, MAJOR]); + UAParser.CPU = enumerize([ARCHITECTURE]); + UAParser.DEVICE = enumerize([MODEL, VENDOR, TYPE, CONSOLE, MOBILE, SMARTTV, TABLET, WEARABLE, EMBEDDED]); + UAParser.ENGINE = UAParser.OS = enumerize([NAME, VERSION]); + + /////////// + // Export + ////////// + + // check js environment + { + // nodejs env + if (module.exports) { + exports = module.exports = UAParser; + } + exports.UAParser = UAParser; + } + + // jQuery/Zepto specific (optional) + // Note: + // In AMD env the global scope should be kept clean, but jQuery is an exception. + // jQuery always exports to global scope, unless jQuery.noConflict(true) is used, + // and we should catch that. + var $ = typeof window !== UNDEF_TYPE && (window.jQuery || window.Zepto); + if ($ && !$.ua) { + var parser = new UAParser(); + $.ua = parser.getResult(); + $.ua.get = function () { + return parser.getUA(); + }; + $.ua.set = function (ua) { + parser.setUA(ua); + var result = parser.getResult(); + for (var prop in result) { + $.ua[prop] = result[prop]; + } + }; + } + + })(typeof window === 'object' ? window : commonjsGlobal); +} (uaParser, uaParser.exports)); + +var uaParserExports = uaParser.exports; + +var Logger$3 = {}; + +var __importDefault$1 = (commonjsGlobal && commonjsGlobal.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(Logger$3, "__esModule", { value: true }); +Logger$3.Logger = void 0; +const debug_1$1 = __importDefault$1(browserExports); +const APP_NAME = 'mediasoup-client'; +class Logger$2 { + constructor(prefix) { + if (prefix) { + this._debug = (0, debug_1$1.default)(`${APP_NAME}:${prefix}`); + this._warn = (0, debug_1$1.default)(`${APP_NAME}:WARN:${prefix}`); + this._error = (0, debug_1$1.default)(`${APP_NAME}:ERROR:${prefix}`); + } + else { + this._debug = (0, debug_1$1.default)(APP_NAME); + this._warn = (0, debug_1$1.default)(`${APP_NAME}:WARN`); + this._error = (0, debug_1$1.default)(`${APP_NAME}:ERROR`); + } + /* eslint-disable no-console */ + this._debug.log = console.info.bind(console); + this._warn.log = console.warn.bind(console); + this._error.log = console.error.bind(console); + /* eslint-enable no-console */ + } + get debug() { + return this._debug; + } + get warn() { + return this._warn; + } + get error() { + return this._error; + } +} +Logger$3.Logger = Logger$2; + +var EnhancedEventEmitter$1 = {}; + +var events = {exports: {}}; + +var R = typeof Reflect === 'object' ? Reflect : null; +var ReflectApply = R && typeof R.apply === 'function' + ? R.apply + : function ReflectApply(target, receiver, args) { + return Function.prototype.apply.call(target, receiver, args); + }; + +var ReflectOwnKeys; +if (R && typeof R.ownKeys === 'function') { + ReflectOwnKeys = R.ownKeys; +} else if (Object.getOwnPropertySymbols) { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target) + .concat(Object.getOwnPropertySymbols(target)); + }; +} else { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target); + }; +} + +function ProcessEmitWarning(warning) { + if (console && console.warn) console.warn(warning); +} + +var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { + return value !== value; +}; + +function EventEmitter() { + EventEmitter.init.call(this); +} +events.exports = EventEmitter; +events.exports.once = once; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._eventsCount = 0; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +var defaultMaxListeners = 10; + +function checkListener(listener) { + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } +} + +Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { + throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); + } + defaultMaxListeners = arg; + } +}); + +EventEmitter.init = function() { + + if (this._events === undefined || + this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; +}; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { + throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); + } + this._maxListeners = n; + return this; +}; + +function _getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; +} + +EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return _getMaxListeners(this); +}; + +EventEmitter.prototype.emit = function emit(type) { + var args = []; + for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); + var doError = (type === 'error'); + + var events = this._events; + if (events !== undefined) + doError = (doError && events.error === undefined); + else if (!doError) + return false; + + // If there is no 'error' event listener then throw. + if (doError) { + var er; + if (args.length > 0) + er = args[0]; + if (er instanceof Error) { + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + // At least give some kind of context to the user + var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); + err.context = er; + throw err; // Unhandled 'error' event + } + + var handler = events[type]; + + if (handler === undefined) + return false; + + if (typeof handler === 'function') { + ReflectApply(handler, this, args); + } else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + ReflectApply(listeners[i], this, args); + } + + return true; +}; + +function _addListener(target, type, listener, prepend) { + var m; + var events; + var existing; + + checkListener(listener); + + events = target._events; + if (events === undefined) { + events = target._events = Object.create(null); + target._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + target.emit('newListener', type, + listener.listener ? listener.listener : listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = target._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = + prepend ? [listener, existing] : [existing, listener]; + // If we've already got an array, just append. + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + + // Check for listener leak + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + // eslint-disable-next-line no-restricted-syntax + var w = new Error('Possible EventEmitter memory leak detected. ' + + existing.length + ' ' + String(type) + ' listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + ProcessEmitWarning(w); + } + } + + return target; +} + +EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; + +function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) + return this.listener.call(this.target); + return this.listener.apply(this.target, arguments); + } +} + +function _onceWrap(target, type, listener) { + var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; + var wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; +} + +EventEmitter.prototype.once = function once(type, listener) { + checkListener(listener); + this.on(type, _onceWrap(this, type, listener)); + return this; +}; + +EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + checkListener(listener); + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + +// Emits a 'removeListener' event if and only if the listener was removed. +EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + var list, events, position, i, originalListener; + + checkListener(listener); + + events = this._events; + if (events === undefined) + return this; + + list = events[type]; + if (list === undefined) + return this; + + if (list === listener || list.listener === listener) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + originalListener = list[i].listener; + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + else { + spliceOne(list, position); + } + + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener !== undefined) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + }; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + var listeners, events, i; + + events = this._events; + if (events === undefined) + return this; + + // not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else + delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + var keys = Object.keys(events); + var key; + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return this; + }; + +function _listeners(target, type, unwrap) { + var events = target._events; + + if (events === undefined) + return []; + + var evlistener = events[type]; + if (evlistener === undefined) + return []; + + if (typeof evlistener === 'function') + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? + unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); +} + +EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); +}; + +EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); +}; + +EventEmitter.listenerCount = function(emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } else { + return listenerCount.call(emitter, type); + } +}; + +EventEmitter.prototype.listenerCount = listenerCount; +function listenerCount(type) { + var events = this._events; + + if (events !== undefined) { + var evlistener = events[type]; + + if (typeof evlistener === 'function') { + return 1; + } else if (evlistener !== undefined) { + return evlistener.length; + } + } + + return 0; +} + +EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; +}; + +function arrayClone(arr, n) { + var copy = new Array(n); + for (var i = 0; i < n; ++i) + copy[i] = arr[i]; + return copy; +} + +function spliceOne(list, index) { + for (; index + 1 < list.length; index++) + list[index] = list[index + 1]; + list.pop(); +} + +function unwrapListeners(arr) { + var ret = new Array(arr.length); + for (var i = 0; i < ret.length; ++i) { + ret[i] = arr[i].listener || arr[i]; + } + return ret; +} + +function once(emitter, name) { + return new Promise(function (resolve, reject) { + function errorListener(err) { + emitter.removeListener(name, resolver); + reject(err); + } + + function resolver() { + if (typeof emitter.removeListener === 'function') { + emitter.removeListener('error', errorListener); + } + resolve([].slice.call(arguments)); + } + eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); + if (name !== 'error') { + addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); + } + }); +} + +function addErrorHandlerIfEventEmitter(emitter, handler, flags) { + if (typeof emitter.on === 'function') { + eventTargetAgnosticAddListener(emitter, 'error', handler, flags); + } +} + +function eventTargetAgnosticAddListener(emitter, name, listener, flags) { + if (typeof emitter.on === 'function') { + if (flags.once) { + emitter.once(name, listener); + } else { + emitter.on(name, listener); + } + } else if (typeof emitter.addEventListener === 'function') { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we do not listen for `error` events here. + emitter.addEventListener(name, function wrapListener(arg) { + // IE does not have builtin `{ once: true }` support so we + // have to do it manually. + if (flags.once) { + emitter.removeEventListener(name, wrapListener); + } + listener(arg); + }); + } else { + throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); + } +} + +var eventsExports = events.exports; + +Object.defineProperty(EnhancedEventEmitter$1, "__esModule", { value: true }); +EnhancedEventEmitter$1.EnhancedEventEmitter = void 0; +const events_1 = eventsExports; +const Logger_1$j = Logger$3; +const logger$j = new Logger_1$j.Logger('EnhancedEventEmitter'); +class EnhancedEventEmitter extends events_1.EventEmitter { + constructor() { + super(); + this.setMaxListeners(Infinity); + } + emit(eventName, ...args) { + return super.emit(eventName, ...args); + } + /** + * Special addition to the EventEmitter API. + */ + safeEmit(eventName, ...args) { + const numListeners = super.listenerCount(eventName); + try { + return super.emit(eventName, ...args); + } + catch (error) { + logger$j.error('safeEmit() | event listener threw an error [eventName:%s]:%o', eventName, error); + return Boolean(numListeners); + } + } + on(eventName, listener) { + super.on(eventName, listener); + return this; + } + off(eventName, listener) { + super.off(eventName, listener); + return this; + } + addListener(eventName, listener) { + super.on(eventName, listener); + return this; + } + prependListener(eventName, listener) { + super.prependListener(eventName, listener); + return this; + } + once(eventName, listener) { + super.once(eventName, listener); + return this; + } + prependOnceListener(eventName, listener) { + super.prependOnceListener(eventName, listener); + return this; + } + removeListener(eventName, listener) { + super.off(eventName, listener); + return this; + } + removeAllListeners(eventName) { + super.removeAllListeners(eventName); + return this; + } + listenerCount(eventName) { + return super.listenerCount(eventName); + } + listeners(eventName) { + return super.listeners(eventName); + } + rawListeners(eventName) { + return super.rawListeners(eventName); + } +} +EnhancedEventEmitter$1.EnhancedEventEmitter = EnhancedEventEmitter; + +var errors = {}; + +Object.defineProperty(errors, "__esModule", { value: true }); +errors.InvalidStateError = errors.UnsupportedError = void 0; +/** + * Error indicating not support for something. + */ +class UnsupportedError extends Error { + constructor(message) { + super(message); + this.name = 'UnsupportedError'; + if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + { + // @ts-ignore + Error.captureStackTrace(this, UnsupportedError); + } + else { + this.stack = (new Error(message)).stack; + } + } +} +errors.UnsupportedError = UnsupportedError; +/** + * Error produced when calling a method in an invalid state. + */ +class InvalidStateError extends Error { + constructor(message) { + super(message); + this.name = 'InvalidStateError'; + if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + { + // @ts-ignore + Error.captureStackTrace(this, InvalidStateError); + } + else { + this.stack = (new Error(message)).stack; + } + } +} +errors.InvalidStateError = InvalidStateError; + +var utils$h = {}; + +Object.defineProperty(utils$h, "__esModule", { value: true }); +utils$h.generateRandomNumber = utils$h.clone = void 0; +/** + * Clones the given value. + */ +function clone(value) { + if (value === undefined) { + return undefined; + } + else if (Number.isNaN(value)) { + return NaN; + } + else if (typeof structuredClone === 'function') { + // Available in Node >= 18. + return structuredClone(value); + } + else { + return JSON.parse(JSON.stringify(value)); + } +} +utils$h.clone = clone; +/** + * Generates a random positive integer. + */ +function generateRandomNumber() { + return Math.round(Math.random() * 10000000); +} +utils$h.generateRandomNumber = generateRandomNumber; + +var ortc$d = {}; + +var h264ProfileLevelId = {}; + +(function (exports) { + const debug = browserExports('h264-profile-level-id'); + const warn = browserExports('h264-profile-level-id:WARN'); + + /* eslint-disable no-console */ + debug.log = console.info.bind(console); + warn.log = console.warn.bind(console); + /* eslint-enable no-console */ + + const ProfileConstrainedBaseline = 1; + const ProfileBaseline = 2; + const ProfileMain = 3; + const ProfileConstrainedHigh = 4; + const ProfileHigh = 5; + + exports.ProfileConstrainedBaseline = ProfileConstrainedBaseline; + exports.ProfileBaseline = ProfileBaseline; + exports.ProfileMain = ProfileMain; + exports.ProfileConstrainedHigh = ProfileConstrainedHigh; + exports.ProfileHigh = ProfileHigh; + + // All values are equal to ten times the level number, except level 1b which is + // special. + const Level1_b = 0; + const Level1 = 10; + const Level1_1 = 11; + const Level1_2 = 12; + const Level1_3 = 13; + const Level2 = 20; + const Level2_1 = 21; + const Level2_2 = 22; + const Level3 = 30; + const Level3_1 = 31; + const Level3_2 = 32; + const Level4 = 40; + const Level4_1 = 41; + const Level4_2 = 42; + const Level5 = 50; + const Level5_1 = 51; + const Level5_2 = 52; + + exports.Level1_b = Level1_b; + exports.Level1 = Level1; + exports.Level1_1 = Level1_1; + exports.Level1_2 = Level1_2; + exports.Level1_3 = Level1_3; + exports.Level2 = Level2; + exports.Level2_1 = Level2_1; + exports.Level2_2 = Level2_2; + exports.Level3 = Level3; + exports.Level3_1 = Level3_1; + exports.Level3_2 = Level3_2; + exports.Level4 = Level4; + exports.Level4_1 = Level4_1; + exports.Level4_2 = Level4_2; + exports.Level5 = Level5; + exports.Level5_1 = Level5_1; + exports.Level5_2 = Level5_2; + + class ProfileLevelId + { + constructor(profile, level) + { + this.profile = profile; + this.level = level; + } + } + + exports.ProfileLevelId = ProfileLevelId; + + // Default ProfileLevelId. + // + // TODO: The default should really be profile Baseline and level 1 according to + // the spec: https://tools.ietf.org/html/rfc6184#section-8.1. In order to not + // break backwards compatibility with older versions of WebRTC where external + // codecs don't have any parameters, use profile ConstrainedBaseline level 3_1 + // instead. This workaround will only be done in an interim period to allow + // external clients to update their code. + // + // http://crbug/webrtc/6337. + const DefaultProfileLevelId = + new ProfileLevelId(ProfileConstrainedBaseline, Level3_1); + + // For level_idc=11 and profile_idc=0x42, 0x4D, or 0x58, the constraint set3 + // flag specifies if level 1b or level 1.1 is used. + const ConstraintSet3Flag = 0x10; + + // Class for matching bit patterns such as "x1xx0000" where 'x' is allowed to + // be either 0 or 1. + class BitPattern + { + constructor(str) + { + this._mask = ~byteMaskString('x', str); + this._maskedValue = byteMaskString('1', str); + } + + isMatch(value) + { + return this._maskedValue === (value & this._mask); + } + } + + // Class for converting between profile_idc/profile_iop to Profile. + class ProfilePattern + { + constructor(profile_idc, profile_iop, profile) + { + this.profile_idc = profile_idc; + this.profile_iop = profile_iop; + this.profile = profile; + } + } + + // This is from https://tools.ietf.org/html/rfc6184#section-8.1. + const ProfilePatterns = + [ + new ProfilePattern(0x42, new BitPattern('x1xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x4D, new BitPattern('1xxx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x58, new BitPattern('11xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x42, new BitPattern('x0xx0000'), ProfileBaseline), + new ProfilePattern(0x58, new BitPattern('10xx0000'), ProfileBaseline), + new ProfilePattern(0x4D, new BitPattern('0x0x0000'), ProfileMain), + new ProfilePattern(0x64, new BitPattern('00000000'), ProfileHigh), + new ProfilePattern(0x64, new BitPattern('00001100'), ProfileConstrainedHigh) + ]; + + /** + * Parse profile level id that is represented as a string of 3 hex bytes. + * Nothing will be returned if the string is not a recognized H264 profile + * level id. + * + * @param {String} str - profile-level-id value as a string of 3 hex bytes. + * + * @returns {ProfileLevelId} + */ + exports.parseProfileLevelId = function(str) + { + // The string should consist of 3 bytes in hexadecimal format. + if (typeof str !== 'string' || str.length !== 6) + { + return null; + } + + const profile_level_id_numeric = parseInt(str, 16); + + if (profile_level_id_numeric === 0) + { + return null; + } + + // Separate into three bytes. + const level_idc = profile_level_id_numeric & 0xFF; + const profile_iop = (profile_level_id_numeric >> 8) & 0xFF; + const profile_idc = (profile_level_id_numeric >> 16) & 0xFF; + + // Parse level based on level_idc and constraint set 3 flag. + let level; + + switch (level_idc) + { + case Level1_1: + { + level = (profile_iop & ConstraintSet3Flag) !== 0 ? Level1_b : Level1_1; + break; + } + case Level1: + case Level1_2: + case Level1_3: + case Level2: + case Level2_1: + case Level2_2: + case Level3: + case Level3_1: + case Level3_2: + case Level4: + case Level4_1: + case Level4_2: + case Level5: + case Level5_1: + case Level5_2: + { + level = level_idc; + break; + } + // Unrecognized level_idc. + default: + { + warn( + `parseProfileLevelId() | unrecognized level_idc [str:${str}, level_idc:${level_idc}]` + ); + + return null; + } + } + + // Parse profile_idc/profile_iop into a Profile enum. + for (const pattern of ProfilePatterns) + { + if ( + profile_idc === pattern.profile_idc && + pattern.profile_iop.isMatch(profile_iop) + ) + { + return new ProfileLevelId(pattern.profile, level); + } + } + + warn( + `parseProfileLevelId() | unrecognized profile_idc/profile_iop combination [str:${str}, profile_idc:${profile_idc}, profile_iop:${profile_iop}]` + ); + + return null; + }; + + /** + * Returns canonical string representation as three hex bytes of the profile + * level id, or returns nothing for invalid profile level ids. + * + * @param {ProfileLevelId} profile_level_id + * + * @returns {String} + */ + exports.profileLevelIdToString = function(profile_level_id) + { + // Handle special case level == 1b. + if (profile_level_id.level == Level1_b) + { + switch (profile_level_id.profile) + { + case ProfileConstrainedBaseline: + { + return '42f00b'; + } + case ProfileBaseline: + { + return '42100b'; + } + case ProfileMain: + { + return '4d100b'; + } + // Level 1_b is not allowed for other profiles. + default: + { + warn( + `profileLevelIdToString() | Level 1_b not is allowed for profile ${profile_level_id.profile}` + ); + + return null; + } + } + } + + let profile_idc_iop_string; + + switch (profile_level_id.profile) + { + case ProfileConstrainedBaseline: + { + profile_idc_iop_string = '42e0'; + break; + } + case ProfileBaseline: + { + profile_idc_iop_string = '4200'; + break; + } + case ProfileMain: + { + profile_idc_iop_string = '4d00'; + break; + } + case ProfileConstrainedHigh: + { + profile_idc_iop_string = '640c'; + break; + } + case ProfileHigh: + { + profile_idc_iop_string = '6400'; + break; + } + default: + { + warn( + `profileLevelIdToString() | unrecognized profile ${profile_level_id.profile}` + ); + + return null; + } + } + + let levelStr = (profile_level_id.level).toString(16); + + if (levelStr.length === 1) + { + levelStr = `0${levelStr}`; + } + + return `${profile_idc_iop_string}${levelStr}`; + }; + + /** + * Parse profile level id that is represented as a string of 3 hex bytes + * contained in an SDP key-value map. A default profile level id will be + * returned if the profile-level-id key is missing. Nothing will be returned + * if the key is present but the string is invalid. + * + * @param {Object} [params={}] - Codec parameters object. + * + * @returns {ProfileLevelId} + */ + exports.parseSdpProfileLevelId = function(params = {}) + { + const profile_level_id = params['profile-level-id']; + + return !profile_level_id + ? DefaultProfileLevelId + : exports.parseProfileLevelId(profile_level_id); + }; + + /** + * Returns true if the parameters have the same H264 profile, i.e. the same + * H264 profile (Baseline, High, etc). + * + * @param {Object} [params1={}] - Codec parameters object. + * @param {Object} [params2={}] - Codec parameters object. + * + * @returns {Boolean} + */ + exports.isSameProfile = function(params1 = {}, params2 = {}) + { + const profile_level_id_1 = exports.parseSdpProfileLevelId(params1); + const profile_level_id_2 = exports.parseSdpProfileLevelId(params2); + + // Compare H264 profiles, but not levels. + return Boolean( + profile_level_id_1 && + profile_level_id_2 && + profile_level_id_1.profile === profile_level_id_2.profile + ); + }; + + /** + * Generate codec parameters that will be used as answer in an SDP negotiation + * based on local supported parameters and remote offered parameters. Both + * local_supported_params and remote_offered_params represent sendrecv media + * descriptions, i.e they are a mix of both encode and decode capabilities. In + * theory, when the profile in local_supported_params represent a strict + * superset of the profile in remote_offered_params, we could limit the profile + * in the answer to the profile in remote_offered_params. + * + * However, to simplify the code, each supported H264 profile should be listed + * explicitly in the list of local supported codecs, even if they are redundant. + * Then each local codec in the list should be tested one at a time against the + * remote codec, and only when the profiles are equal should this function be + * called. Therefore, this function does not need to handle profile intersection, + * and the profile of local_supported_params and remote_offered_params must be + * equal before calling this function. The parameters that are used when + * negotiating are the level part of profile-level-id and level-asymmetry-allowed. + * + * @param {Object} [local_supported_params={}] + * @param {Object} [remote_offered_params={}] + * + * @returns {String} Canonical string representation as three hex bytes of the + * profile level id, or null if no one of the params have profile-level-id. + * + * @throws {TypeError} If Profile mismatch or invalid params. + */ + exports.generateProfileLevelIdForAnswer = function( + local_supported_params = {}, + remote_offered_params = {} + ) + { + // If both local and remote params do not contain profile-level-id, they are + // both using the default profile. In this case, don't return anything. + if ( + !local_supported_params['profile-level-id'] && + !remote_offered_params['profile-level-id'] + ) + { + warn( + 'generateProfileLevelIdForAnswer() | profile-level-id missing in local and remote params' + ); + + return null; + } + + // Parse profile-level-ids. + const local_profile_level_id = + exports.parseSdpProfileLevelId(local_supported_params); + const remote_profile_level_id = + exports.parseSdpProfileLevelId(remote_offered_params); + + // The local and remote codec must have valid and equal H264 Profiles. + if (!local_profile_level_id) + { + throw new TypeError('invalid local_profile_level_id'); + } + + if (!remote_profile_level_id) + { + throw new TypeError('invalid remote_profile_level_id'); + } + + if (local_profile_level_id.profile !== remote_profile_level_id.profile) + { + throw new TypeError('H264 Profile mismatch'); + } + + // Parse level information. + const level_asymmetry_allowed = ( + isLevelAsymmetryAllowed(local_supported_params) && + isLevelAsymmetryAllowed(remote_offered_params) + ); + + const local_level = local_profile_level_id.level; + const remote_level = remote_profile_level_id.level; + const min_level = minLevel(local_level, remote_level); + + // Determine answer level. When level asymmetry is not allowed, level upgrade + // is not allowed, i.e., the level in the answer must be equal to or lower + // than the level in the offer. + const answer_level = level_asymmetry_allowed ? local_level : min_level; + + debug( + `generateProfileLevelIdForAnswer() | result [profile:${local_profile_level_id.profile}, level:${answer_level}]` + ); + + // Return the resulting profile-level-id for the answer parameters. + return exports.profileLevelIdToString( + new ProfileLevelId(local_profile_level_id.profile, answer_level)); + }; + + // Convert a string of 8 characters into a byte where the positions containing + // character c will have their bit set. For example, c = 'x', str = "x1xx0000" + // will return 0b10110000. + function byteMaskString(c, str) + { + return ( + ((str[0] === c) << 7) | ((str[1] === c) << 6) | ((str[2] === c) << 5) | + ((str[3] === c) << 4) | ((str[4] === c) << 3) | ((str[5] === c) << 2) | + ((str[6] === c) << 1) | ((str[7] === c) << 0) + ); + } + + // Compare H264 levels and handle the level 1b case. + function isLessLevel(a, b) + { + if (a === Level1_b) + { + return b !== Level1 && b !== Level1_b; + } + + if (b === Level1_b) + { + return a !== Level1; + } + + return a < b; + } + + function minLevel(a, b) + { + return isLessLevel(a, b) ? a : b; + } + + function isLevelAsymmetryAllowed(params = {}) + { + const level_asymmetry_allowed = params['level-asymmetry-allowed']; + + return ( + level_asymmetry_allowed === 1 || + level_asymmetry_allowed === '1' + ); + } +} (h264ProfileLevelId)); + +var __createBinding$h = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$h = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$h = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$h(result, mod, k); + __setModuleDefault$h(result, mod); + return result; +}; +Object.defineProperty(ortc$d, "__esModule", { value: true }); +ortc$d.canReceive = ortc$d.canSend = ortc$d.generateProbatorRtpParameters = ortc$d.reduceCodecs = ortc$d.getSendingRemoteRtpParameters = ortc$d.getSendingRtpParameters = ortc$d.getRecvRtpCapabilities = ortc$d.getExtendedRtpCapabilities = ortc$d.validateSctpStreamParameters = ortc$d.validateSctpParameters = ortc$d.validateNumSctpStreams = ortc$d.validateSctpCapabilities = ortc$d.validateRtcpParameters = ortc$d.validateRtpEncodingParameters = ortc$d.validateRtpHeaderExtensionParameters = ortc$d.validateRtpCodecParameters = ortc$d.validateRtpParameters = ortc$d.validateRtpHeaderExtension = ortc$d.validateRtcpFeedback = ortc$d.validateRtpCodecCapability = ortc$d.validateRtpCapabilities = void 0; +const h264 = __importStar$h(h264ProfileLevelId); +const utils$g = __importStar$h(utils$h); +const RTP_PROBATOR_MID = 'probator'; +const RTP_PROBATOR_SSRC = 1234; +const RTP_PROBATOR_CODEC_PAYLOAD_TYPE = 127; +/** + * Validates RtpCapabilities. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpCapabilities(caps) { + if (typeof caps !== 'object') { + throw new TypeError('caps is not an object'); + } + // codecs is optional. If unset, fill with an empty array. + if (caps.codecs && !Array.isArray(caps.codecs)) { + throw new TypeError('caps.codecs is not an array'); + } + else if (!caps.codecs) { + caps.codecs = []; + } + for (const codec of caps.codecs) { + validateRtpCodecCapability(codec); + } + // headerExtensions is optional. If unset, fill with an empty array. + if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) { + throw new TypeError('caps.headerExtensions is not an array'); + } + else if (!caps.headerExtensions) { + caps.headerExtensions = []; + } + for (const ext of caps.headerExtensions) { + validateRtpHeaderExtension(ext); + } +} +ortc$d.validateRtpCapabilities = validateRtpCapabilities; +/** + * Validates RtpCodecCapability. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpCodecCapability(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + if (typeof codec !== 'object') { + throw new TypeError('codec is not an object'); + } + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') { + throw new TypeError('missing codec.mimeType'); + } + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) { + throw new TypeError('invalid codec.mimeType'); + } + // Just override kind with media component of mimeType. + codec.kind = mimeTypeMatch[1].toLowerCase(); + // preferredPayloadType is optional. + if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') { + throw new TypeError('invalid codec.preferredPayloadType'); + } + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') { + throw new TypeError('missing codec.clockRate'); + } + // channels is optional. If unset, set it to 1 (just if audio). + if (codec.kind === 'audio') { + if (typeof codec.channels !== 'number') { + codec.channels = 1; + } + } + else { + delete codec.channels; + } + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') { + codec.parameters = {}; + } + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`); + } + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') { + throw new TypeError('invalid codec apt parameter'); + } + } + } + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { + codec.rtcpFeedback = []; + } + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } +} +ortc$d.validateRtpCodecCapability = validateRtpCodecCapability; +/** + * Validates RtcpFeedback. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtcpFeedback(fb) { + if (typeof fb !== 'object') { + throw new TypeError('fb is not an object'); + } + // type is mandatory. + if (!fb.type || typeof fb.type !== 'string') { + throw new TypeError('missing fb.type'); + } + // parameter is optional. If unset set it to an empty string. + if (!fb.parameter || typeof fb.parameter !== 'string') { + fb.parameter = ''; + } +} +ortc$d.validateRtcpFeedback = validateRtcpFeedback; +/** + * Validates RtpHeaderExtension. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpHeaderExtension(ext) { + if (typeof ext !== 'object') { + throw new TypeError('ext is not an object'); + } + // kind is mandatory. + if (ext.kind !== 'audio' && ext.kind !== 'video') { + throw new TypeError('invalid ext.kind'); + } + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') { + throw new TypeError('missing ext.uri'); + } + // preferredId is mandatory. + if (typeof ext.preferredId !== 'number') { + throw new TypeError('missing ext.preferredId'); + } + // preferredEncrypt is optional. If unset set it to false. + if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') { + throw new TypeError('invalid ext.preferredEncrypt'); + } + else if (!ext.preferredEncrypt) { + ext.preferredEncrypt = false; + } + // direction is optional. If unset set it to sendrecv. + if (ext.direction && typeof ext.direction !== 'string') { + throw new TypeError('invalid ext.direction'); + } + else if (!ext.direction) { + ext.direction = 'sendrecv'; + } +} +ortc$d.validateRtpHeaderExtension = validateRtpHeaderExtension; +/** + * Validates RtpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpParameters(params) { + if (typeof params !== 'object') { + throw new TypeError('params is not an object'); + } + // mid is optional. + if (params.mid && typeof params.mid !== 'string') { + throw new TypeError('params.mid is not a string'); + } + // codecs is mandatory. + if (!Array.isArray(params.codecs)) { + throw new TypeError('missing params.codecs'); + } + for (const codec of params.codecs) { + validateRtpCodecParameters(codec); + } + // headerExtensions is optional. If unset, fill with an empty array. + if (params.headerExtensions && !Array.isArray(params.headerExtensions)) { + throw new TypeError('params.headerExtensions is not an array'); + } + else if (!params.headerExtensions) { + params.headerExtensions = []; + } + for (const ext of params.headerExtensions) { + validateRtpHeaderExtensionParameters(ext); + } + // encodings is optional. If unset, fill with an empty array. + if (params.encodings && !Array.isArray(params.encodings)) { + throw new TypeError('params.encodings is not an array'); + } + else if (!params.encodings) { + params.encodings = []; + } + for (const encoding of params.encodings) { + validateRtpEncodingParameters(encoding); + } + // rtcp is optional. If unset, fill with an empty object. + if (params.rtcp && typeof params.rtcp !== 'object') { + throw new TypeError('params.rtcp is not an object'); + } + else if (!params.rtcp) { + params.rtcp = {}; + } + validateRtcpParameters(params.rtcp); +} +ortc$d.validateRtpParameters = validateRtpParameters; +/** + * Validates RtpCodecParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpCodecParameters(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + if (typeof codec !== 'object') { + throw new TypeError('codec is not an object'); + } + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') { + throw new TypeError('missing codec.mimeType'); + } + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) { + throw new TypeError('invalid codec.mimeType'); + } + // payloadType is mandatory. + if (typeof codec.payloadType !== 'number') { + throw new TypeError('missing codec.payloadType'); + } + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') { + throw new TypeError('missing codec.clockRate'); + } + const kind = mimeTypeMatch[1].toLowerCase(); + // channels is optional. If unset, set it to 1 (just if audio). + if (kind === 'audio') { + if (typeof codec.channels !== 'number') { + codec.channels = 1; + } + } + else { + delete codec.channels; + } + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') { + codec.parameters = {}; + } + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`); + } + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') { + throw new TypeError('invalid codec apt parameter'); + } + } + } + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { + codec.rtcpFeedback = []; + } + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } +} +ortc$d.validateRtpCodecParameters = validateRtpCodecParameters; +/** + * Validates RtpHeaderExtensionParameteters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpHeaderExtensionParameters(ext) { + if (typeof ext !== 'object') { + throw new TypeError('ext is not an object'); + } + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') { + throw new TypeError('missing ext.uri'); + } + // id is mandatory. + if (typeof ext.id !== 'number') { + throw new TypeError('missing ext.id'); + } + // encrypt is optional. If unset set it to false. + if (ext.encrypt && typeof ext.encrypt !== 'boolean') { + throw new TypeError('invalid ext.encrypt'); + } + else if (!ext.encrypt) { + ext.encrypt = false; + } + // parameters is optional. If unset, set it to an empty object. + if (!ext.parameters || typeof ext.parameters !== 'object') { + ext.parameters = {}; + } + for (const key of Object.keys(ext.parameters)) { + let value = ext.parameters[key]; + if (value === undefined) { + ext.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError('invalid header extension parameter'); + } + } +} +ortc$d.validateRtpHeaderExtensionParameters = validateRtpHeaderExtensionParameters; +/** + * Validates RtpEncodingParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpEncodingParameters(encoding) { + if (typeof encoding !== 'object') { + throw new TypeError('encoding is not an object'); + } + // ssrc is optional. + if (encoding.ssrc && typeof encoding.ssrc !== 'number') { + throw new TypeError('invalid encoding.ssrc'); + } + // rid is optional. + if (encoding.rid && typeof encoding.rid !== 'string') { + throw new TypeError('invalid encoding.rid'); + } + // rtx is optional. + if (encoding.rtx && typeof encoding.rtx !== 'object') { + throw new TypeError('invalid encoding.rtx'); + } + else if (encoding.rtx) { + // RTX ssrc is mandatory if rtx is present. + if (typeof encoding.rtx.ssrc !== 'number') { + throw new TypeError('missing encoding.rtx.ssrc'); + } + } + // dtx is optional. If unset set it to false. + if (!encoding.dtx || typeof encoding.dtx !== 'boolean') { + encoding.dtx = false; + } + // scalabilityMode is optional. + if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') { + throw new TypeError('invalid encoding.scalabilityMode'); + } +} +ortc$d.validateRtpEncodingParameters = validateRtpEncodingParameters; +/** + * Validates RtcpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtcpParameters(rtcp) { + if (typeof rtcp !== 'object') { + throw new TypeError('rtcp is not an object'); + } + // cname is optional. + if (rtcp.cname && typeof rtcp.cname !== 'string') { + throw new TypeError('invalid rtcp.cname'); + } + // reducedSize is optional. If unset set it to true. + if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') { + rtcp.reducedSize = true; + } +} +ortc$d.validateRtcpParameters = validateRtcpParameters; +/** + * Validates SctpCapabilities. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateSctpCapabilities(caps) { + if (typeof caps !== 'object') { + throw new TypeError('caps is not an object'); + } + // numStreams is mandatory. + if (!caps.numStreams || typeof caps.numStreams !== 'object') { + throw new TypeError('missing caps.numStreams'); + } + validateNumSctpStreams(caps.numStreams); +} +ortc$d.validateSctpCapabilities = validateSctpCapabilities; +/** + * Validates NumSctpStreams. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateNumSctpStreams(numStreams) { + if (typeof numStreams !== 'object') { + throw new TypeError('numStreams is not an object'); + } + // OS is mandatory. + if (typeof numStreams.OS !== 'number') { + throw new TypeError('missing numStreams.OS'); + } + // MIS is mandatory. + if (typeof numStreams.MIS !== 'number') { + throw new TypeError('missing numStreams.MIS'); + } +} +ortc$d.validateNumSctpStreams = validateNumSctpStreams; +/** + * Validates SctpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateSctpParameters(params) { + if (typeof params !== 'object') { + throw new TypeError('params is not an object'); + } + // port is mandatory. + if (typeof params.port !== 'number') { + throw new TypeError('missing params.port'); + } + // OS is mandatory. + if (typeof params.OS !== 'number') { + throw new TypeError('missing params.OS'); + } + // MIS is mandatory. + if (typeof params.MIS !== 'number') { + throw new TypeError('missing params.MIS'); + } + // maxMessageSize is mandatory. + if (typeof params.maxMessageSize !== 'number') { + throw new TypeError('missing params.maxMessageSize'); + } +} +ortc$d.validateSctpParameters = validateSctpParameters; +/** + * Validates SctpStreamParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateSctpStreamParameters(params) { + if (typeof params !== 'object') { + throw new TypeError('params is not an object'); + } + // streamId is mandatory. + if (typeof params.streamId !== 'number') { + throw new TypeError('missing params.streamId'); + } + // ordered is optional. + let orderedGiven = false; + if (typeof params.ordered === 'boolean') { + orderedGiven = true; + } + else { + params.ordered = true; + } + // maxPacketLifeTime is optional. + if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') { + throw new TypeError('invalid params.maxPacketLifeTime'); + } + // maxRetransmits is optional. + if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') { + throw new TypeError('invalid params.maxRetransmits'); + } + if (params.maxPacketLifeTime && params.maxRetransmits) { + throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits'); + } + if (orderedGiven && + params.ordered && + (params.maxPacketLifeTime || params.maxRetransmits)) { + throw new TypeError('cannot be ordered with maxPacketLifeTime or maxRetransmits'); + } + else if (!orderedGiven && (params.maxPacketLifeTime || params.maxRetransmits)) { + params.ordered = false; + } + // label is optional. + if (params.label && typeof params.label !== 'string') { + throw new TypeError('invalid params.label'); + } + // protocol is optional. + if (params.protocol && typeof params.protocol !== 'string') { + throw new TypeError('invalid params.protocol'); + } +} +ortc$d.validateSctpStreamParameters = validateSctpStreamParameters; +/** + * Generate extended RTP capabilities for sending and receiving. + */ +function getExtendedRtpCapabilities(localCaps, remoteCaps) { + const extendedRtpCapabilities = { + codecs: [], + headerExtensions: [] + }; + // Match media codecs and keep the order preferred by remoteCaps. + for (const remoteCodec of remoteCaps.codecs || []) { + if (isRtxCodec(remoteCodec)) { + continue; + } + const matchingLocalCodec = (localCaps.codecs || []) + .find((localCodec) => (matchCodecs(localCodec, remoteCodec, { strict: true, modify: true }))); + if (!matchingLocalCodec) { + continue; + } + const extendedCodec = { + mimeType: matchingLocalCodec.mimeType, + kind: matchingLocalCodec.kind, + clockRate: matchingLocalCodec.clockRate, + channels: matchingLocalCodec.channels, + localPayloadType: matchingLocalCodec.preferredPayloadType, + localRtxPayloadType: undefined, + remotePayloadType: remoteCodec.preferredPayloadType, + remoteRtxPayloadType: undefined, + localParameters: matchingLocalCodec.parameters, + remoteParameters: remoteCodec.parameters, + rtcpFeedback: reduceRtcpFeedback(matchingLocalCodec, remoteCodec) + }; + extendedRtpCapabilities.codecs.push(extendedCodec); + } + // Match RTX codecs. + for (const extendedCodec of extendedRtpCapabilities.codecs) { + const matchingLocalRtxCodec = localCaps.codecs + .find((localCodec) => (isRtxCodec(localCodec) && + localCodec.parameters.apt === extendedCodec.localPayloadType)); + const matchingRemoteRtxCodec = remoteCaps.codecs + .find((remoteCodec) => (isRtxCodec(remoteCodec) && + remoteCodec.parameters.apt === extendedCodec.remotePayloadType)); + if (matchingLocalRtxCodec && matchingRemoteRtxCodec) { + extendedCodec.localRtxPayloadType = matchingLocalRtxCodec.preferredPayloadType; + extendedCodec.remoteRtxPayloadType = matchingRemoteRtxCodec.preferredPayloadType; + } + } + // Match header extensions. + for (const remoteExt of remoteCaps.headerExtensions) { + const matchingLocalExt = localCaps.headerExtensions + .find((localExt) => (matchHeaderExtensions(localExt, remoteExt))); + if (!matchingLocalExt) { + continue; + } + const extendedExt = { + kind: remoteExt.kind, + uri: remoteExt.uri, + sendId: matchingLocalExt.preferredId, + recvId: remoteExt.preferredId, + encrypt: matchingLocalExt.preferredEncrypt, + direction: 'sendrecv' + }; + switch (remoteExt.direction) { + case 'sendrecv': + extendedExt.direction = 'sendrecv'; + break; + case 'recvonly': + extendedExt.direction = 'sendonly'; + break; + case 'sendonly': + extendedExt.direction = 'recvonly'; + break; + case 'inactive': + extendedExt.direction = 'inactive'; + break; + } + extendedRtpCapabilities.headerExtensions.push(extendedExt); + } + return extendedRtpCapabilities; +} +ortc$d.getExtendedRtpCapabilities = getExtendedRtpCapabilities; +/** + * Generate RTP capabilities for receiving media based on the given extended + * RTP capabilities. + */ +function getRecvRtpCapabilities(extendedRtpCapabilities) { + const rtpCapabilities = { + codecs: [], + headerExtensions: [] + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + const codec = { + mimeType: extendedCodec.mimeType, + kind: extendedCodec.kind, + preferredPayloadType: extendedCodec.remotePayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.localParameters, + rtcpFeedback: extendedCodec.rtcpFeedback + }; + rtpCapabilities.codecs.push(codec); + // Add RTX codec. + if (!extendedCodec.remoteRtxPayloadType) { + continue; + } + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + kind: extendedCodec.kind, + preferredPayloadType: extendedCodec.remoteRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.remotePayloadType + }, + rtcpFeedback: [] + }; + rtpCapabilities.codecs.push(rtxCodec); + // TODO: In the future, we need to add FEC, CN, etc, codecs. + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions not valid for receiving. + if (extendedExtension.direction !== 'sendrecv' && + extendedExtension.direction !== 'recvonly') { + continue; + } + const ext = { + kind: extendedExtension.kind, + uri: extendedExtension.uri, + preferredId: extendedExtension.recvId, + preferredEncrypt: extendedExtension.encrypt, + direction: extendedExtension.direction + }; + rtpCapabilities.headerExtensions.push(ext); + } + return rtpCapabilities; +} +ortc$d.getRecvRtpCapabilities = getRecvRtpCapabilities; +/** + * Generate RTP parameters of the given kind for sending media. + * NOTE: mid, encodings and rtcp fields are left empty. + */ +function getSendingRtpParameters(kind, extendedRtpCapabilities) { + const rtpParameters = { + mid: undefined, + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: {} + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + if (extendedCodec.kind !== kind) { + continue; + } + const codec = { + mimeType: extendedCodec.mimeType, + payloadType: extendedCodec.localPayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.localParameters, + rtcpFeedback: extendedCodec.rtcpFeedback + }; + rtpParameters.codecs.push(codec); + // Add RTX codec. + if (extendedCodec.localRtxPayloadType) { + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + payloadType: extendedCodec.localRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.localPayloadType + }, + rtcpFeedback: [] + }; + rtpParameters.codecs.push(rtxCodec); + } + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions of a different kind and those not valid for sending. + if ((extendedExtension.kind && extendedExtension.kind !== kind) || + (extendedExtension.direction !== 'sendrecv' && + extendedExtension.direction !== 'sendonly')) { + continue; + } + const ext = { + uri: extendedExtension.uri, + id: extendedExtension.sendId, + encrypt: extendedExtension.encrypt, + parameters: {} + }; + rtpParameters.headerExtensions.push(ext); + } + return rtpParameters; +} +ortc$d.getSendingRtpParameters = getSendingRtpParameters; +/** + * Generate RTP parameters of the given kind suitable for the remote SDP answer. + */ +function getSendingRemoteRtpParameters(kind, extendedRtpCapabilities) { + const rtpParameters = { + mid: undefined, + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: {} + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + if (extendedCodec.kind !== kind) { + continue; + } + const codec = { + mimeType: extendedCodec.mimeType, + payloadType: extendedCodec.localPayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.remoteParameters, + rtcpFeedback: extendedCodec.rtcpFeedback + }; + rtpParameters.codecs.push(codec); + // Add RTX codec. + if (extendedCodec.localRtxPayloadType) { + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + payloadType: extendedCodec.localRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.localPayloadType + }, + rtcpFeedback: [] + }; + rtpParameters.codecs.push(rtxCodec); + } + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions of a different kind and those not valid for sending. + if ((extendedExtension.kind && extendedExtension.kind !== kind) || + (extendedExtension.direction !== 'sendrecv' && + extendedExtension.direction !== 'sendonly')) { + continue; + } + const ext = { + uri: extendedExtension.uri, + id: extendedExtension.sendId, + encrypt: extendedExtension.encrypt, + parameters: {} + }; + rtpParameters.headerExtensions.push(ext); + } + // Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise. + if (rtpParameters.headerExtensions.some((ext) => (ext.uri === 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01'))) { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []) + .filter((fb) => fb.type !== 'goog-remb'); + } + } + else if (rtpParameters.headerExtensions.some((ext) => (ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time'))) { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []) + .filter((fb) => fb.type !== 'transport-cc'); + } + } + else { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []) + .filter((fb) => (fb.type !== 'transport-cc' && + fb.type !== 'goog-remb')); + } + } + return rtpParameters; +} +ortc$d.getSendingRemoteRtpParameters = getSendingRemoteRtpParameters; +/** + * Reduce given codecs by returning an array of codecs "compatible" with the + * given capability codec. If no capability codec is given, take the first + * one(s). + * + * Given codecs must be generated by ortc.getSendingRtpParameters() or + * ortc.getSendingRemoteRtpParameters(). + * + * The returned array of codecs also include a RTX codec if available. + */ +function reduceCodecs(codecs, capCodec) { + const filteredCodecs = []; + // If no capability codec is given, take the first one (and RTX). + if (!capCodec) { + filteredCodecs.push(codecs[0]); + if (isRtxCodec(codecs[1])) { + filteredCodecs.push(codecs[1]); + } + } + // Otherwise look for a compatible set of codecs. + else { + for (let idx = 0; idx < codecs.length; ++idx) { + if (matchCodecs(codecs[idx], capCodec)) { + filteredCodecs.push(codecs[idx]); + if (isRtxCodec(codecs[idx + 1])) { + filteredCodecs.push(codecs[idx + 1]); + } + break; + } + } + if (filteredCodecs.length === 0) { + throw new TypeError('no matching codec found'); + } + } + return filteredCodecs; +} +ortc$d.reduceCodecs = reduceCodecs; +/** + * Create RTP parameters for a Consumer for the RTP probator. + */ +function generateProbatorRtpParameters(videoRtpParameters) { + // Clone given reference video RTP parameters. + videoRtpParameters = utils$g.clone(videoRtpParameters); + // This may throw. + validateRtpParameters(videoRtpParameters); + const rtpParameters = { + mid: RTP_PROBATOR_MID, + codecs: [], + headerExtensions: [], + encodings: [{ ssrc: RTP_PROBATOR_SSRC }], + rtcp: { cname: 'probator' } + }; + rtpParameters.codecs.push(videoRtpParameters.codecs[0]); + rtpParameters.codecs[0].payloadType = RTP_PROBATOR_CODEC_PAYLOAD_TYPE; + rtpParameters.headerExtensions = videoRtpParameters.headerExtensions; + return rtpParameters; +} +ortc$d.generateProbatorRtpParameters = generateProbatorRtpParameters; +/** + * Whether media can be sent based on the given RTP capabilities. + */ +function canSend(kind, extendedRtpCapabilities) { + return extendedRtpCapabilities.codecs. + some((codec) => codec.kind === kind); +} +ortc$d.canSend = canSend; +/** + * Whether the given RTP parameters can be received with the given RTP + * capabilities. + */ +function canReceive(rtpParameters, extendedRtpCapabilities) { + // This may throw. + validateRtpParameters(rtpParameters); + if (rtpParameters.codecs.length === 0) { + return false; + } + const firstMediaCodec = rtpParameters.codecs[0]; + return extendedRtpCapabilities.codecs + .some((codec) => codec.remotePayloadType === firstMediaCodec.payloadType); +} +ortc$d.canReceive = canReceive; +function isRtxCodec(codec) { + if (!codec) { + return false; + } + return /.+\/rtx$/i.test(codec.mimeType); +} +function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) { + const aMimeType = aCodec.mimeType.toLowerCase(); + const bMimeType = bCodec.mimeType.toLowerCase(); + if (aMimeType !== bMimeType) { + return false; + } + if (aCodec.clockRate !== bCodec.clockRate) { + return false; + } + if (aCodec.channels !== bCodec.channels) { + return false; + } + // Per codec special checks. + switch (aMimeType) { + case 'video/h264': + { + if (strict) { + const aPacketizationMode = aCodec.parameters['packetization-mode'] || 0; + const bPacketizationMode = bCodec.parameters['packetization-mode'] || 0; + if (aPacketizationMode !== bPacketizationMode) { + return false; + } + if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) { + return false; + } + let selectedProfileLevelId; + try { + selectedProfileLevelId = + h264.generateProfileLevelIdForAnswer(aCodec.parameters, bCodec.parameters); + } + catch (error) { + return false; + } + if (modify) { + if (selectedProfileLevelId) { + aCodec.parameters['profile-level-id'] = selectedProfileLevelId; + bCodec.parameters['profile-level-id'] = selectedProfileLevelId; + } + else { + delete aCodec.parameters['profile-level-id']; + delete bCodec.parameters['profile-level-id']; + } + } + } + break; + } + case 'video/vp9': + { + if (strict) { + const aProfileId = aCodec.parameters['profile-id'] || 0; + const bProfileId = bCodec.parameters['profile-id'] || 0; + if (aProfileId !== bProfileId) { + return false; + } + } + break; + } + } + return true; +} +function matchHeaderExtensions(aExt, bExt) { + if (aExt.kind && bExt.kind && aExt.kind !== bExt.kind) { + return false; + } + if (aExt.uri !== bExt.uri) { + return false; + } + return true; +} +function reduceRtcpFeedback(codecA, codecB) { + const reducedRtcpFeedback = []; + for (const aFb of codecA.rtcpFeedback || []) { + const matchingBFb = (codecB.rtcpFeedback || []) + .find((bFb) => (bFb.type === aFb.type && + (bFb.parameter === aFb.parameter || (!bFb.parameter && !aFb.parameter)))); + if (matchingBFb) { + reducedRtcpFeedback.push(matchingBFb); + } + } + return reducedRtcpFeedback; +} + +var Transport$1 = {}; + +var lib$1 = {}; + +var Logger$1 = {}; + +Object.defineProperty(Logger$1, "__esModule", { value: true }); +Logger$1.Logger = void 0; +const debug_1 = browserExports; +const LIB_NAME = 'awaitqueue'; +class Logger { + constructor(prefix) { + if (prefix) { + this._debug = (0, debug_1.default)(`${LIB_NAME}:${prefix}`); + this._warn = (0, debug_1.default)(`${LIB_NAME}:WARN:${prefix}`); + this._error = (0, debug_1.default)(`${LIB_NAME}:ERROR:${prefix}`); + } + else { + this._debug = (0, debug_1.default)(LIB_NAME); + this._warn = (0, debug_1.default)(`${LIB_NAME}:WARN`); + this._error = (0, debug_1.default)(`${LIB_NAME}:ERROR`); + } + /* eslint-disable no-console */ + this._debug.log = console.info.bind(console); + this._warn.log = console.warn.bind(console); + this._error.log = console.error.bind(console); + /* eslint-enable no-console */ + } + get debug() { + return this._debug; + } + get warn() { + return this._warn; + } + get error() { + return this._error; + } +} +Logger$1.Logger = Logger; + +Object.defineProperty(lib$1, "__esModule", { value: true }); +lib$1.AwaitQueue = lib$1.AwaitQueueRemovedTaskError = lib$1.AwaitQueueStoppedError = void 0; +const Logger_1$i = Logger$1; +const logger$i = new Logger_1$i.Logger(); +/** + * Custom Error derived class used to reject pending tasks once stop() method + * has been called. + */ +class AwaitQueueStoppedError extends Error { + constructor(message) { + super(message ?? 'AwaitQueue stopped'); + this.name = 'AwaitQueueStoppedError'; + // @ts-ignore + if (typeof Error.captureStackTrace === 'function') { + // @ts-ignore + Error.captureStackTrace(this, AwaitQueueStoppedError); + } + } +} +lib$1.AwaitQueueStoppedError = AwaitQueueStoppedError; +/** + * Custom Error derived class used to reject pending tasks once removeTask() + * method has been called. + */ +class AwaitQueueRemovedTaskError extends Error { + constructor(message) { + super(message ?? 'AwaitQueue task removed'); + this.name = 'AwaitQueueRemovedTaskError'; + // @ts-ignore + if (typeof Error.captureStackTrace === 'function') { + // @ts-ignore + Error.captureStackTrace(this, AwaitQueueRemovedTaskError); + } + } +} +lib$1.AwaitQueueRemovedTaskError = AwaitQueueRemovedTaskError; +class AwaitQueue { + constructor() { + // Queue of pending tasks (map of PendingTasks indexed by id). + this.pendingTasks = new Map(); + // Incrementing PendingTask id. + this.nextTaskId = 0; + // Whether stop() method is stopping all pending tasks. + this.stopping = false; + } + get size() { + return this.pendingTasks.size; + } + async push(task, name) { + name = name ?? task.name; + logger$i.debug(`push() [name:${name}]`); + if (typeof task !== 'function') { + throw new TypeError('given task is not a function'); + } + if (name) { + try { + Object.defineProperty(task, 'name', { value: name }); + } + catch (error) { } + } + return new Promise((resolve, reject) => { + const pendingTask = { + id: this.nextTaskId++, + task: task, + name: name, + enqueuedAt: Date.now(), + executedAt: undefined, + completed: false, + resolve: (result) => { + // pendingTask.resolve() can only be called in execute() method. Since + // resolve() was called it means that the task successfully completed. + // However the task may have been stopped before it completed (via + // stop() or remove()) so its completed flag was already set. If this + // is the case, abort here since next task (if any) is already being + // executed. + if (pendingTask.completed) { + return; + } + pendingTask.completed = true; + // Remove the task from the queue. + this.pendingTasks.delete(pendingTask.id); + logger$i.debug(`resolving task [name:${pendingTask.name}]`); + // Resolve the task with the obtained result. + resolve(result); + // Execute the next pending task (if any). + const [nextPendingTask] = this.pendingTasks.values(); + // NOTE: During the resolve() callback the user app may have interacted + // with the queue. For instance, the app may have pushed a task while + // the queue was empty so such a task is already being executed. If so, + // don't execute it twice. + if (nextPendingTask && !nextPendingTask.executedAt) { + void this.execute(nextPendingTask); + } + }, + reject: (error) => { + // pendingTask.reject() can be called within execute() method if the + // task completed with error. However it may have also been called in + // stop() or remove() methods (before or while being executed) so its + // completed flag was already set. If so, abort here since next task + // (if any) is already being executed. + if (pendingTask.completed) { + return; + } + pendingTask.completed = true; + // Remove the task from the queue. + this.pendingTasks.delete(pendingTask.id); + logger$i.debug(`rejecting task [name:${pendingTask.name}]: %s`, String(error)); + // Reject the task with the obtained error. + reject(error); + // Execute the next pending task (if any) unless stop() is running. + if (!this.stopping) { + const [nextPendingTask] = this.pendingTasks.values(); + // NOTE: During the reject() callback the user app may have interacted + // with the queue. For instance, the app may have pushed a task while + // the queue was empty so such a task is already being executed. If so, + // don't execute it twice. + if (nextPendingTask && !nextPendingTask.executedAt) { + void this.execute(nextPendingTask); + } + } + } + }; + // Append task to the queue. + this.pendingTasks.set(pendingTask.id, pendingTask); + // And execute it if this is the only task in the queue. + if (this.pendingTasks.size === 1) { + void this.execute(pendingTask); + } + }); + } + stop() { + logger$i.debug('stop()'); + this.stopping = true; + for (const pendingTask of this.pendingTasks.values()) { + logger$i.debug(`stop() | stopping task [name:${pendingTask.name}]`); + pendingTask.reject(new AwaitQueueStoppedError()); + } + this.stopping = false; + } + remove(taskIdx) { + logger$i.debug(`remove() [taskIdx:${taskIdx}]`); + const pendingTask = Array.from(this.pendingTasks.values())[taskIdx]; + if (!pendingTask) { + logger$i.debug(`stop() | no task with given idx [taskIdx:${taskIdx}]`); + return; + } + pendingTask.reject(new AwaitQueueRemovedTaskError()); + } + dump() { + const now = Date.now(); + let idx = 0; + return Array.from(this.pendingTasks.values()).map((pendingTask) => ({ + idx: idx++, + task: pendingTask.task, + name: pendingTask.name, + enqueuedTime: pendingTask.executedAt + ? pendingTask.executedAt - pendingTask.enqueuedAt + : now - pendingTask.enqueuedAt, + executionTime: pendingTask.executedAt + ? now - pendingTask.executedAt + : 0 + })); + } + async execute(pendingTask) { + logger$i.debug(`execute() [name:${pendingTask.name}]`); + if (pendingTask.executedAt) { + throw new Error('task already being executed'); + } + pendingTask.executedAt = Date.now(); + try { + const result = await pendingTask.task(); + // Resolve the task with its resolved result (if any). + pendingTask.resolve(result); + } + catch (error) { + // Reject the task with its rejected error. + pendingTask.reject(error); + } + } +} +lib$1.AwaitQueue = AwaitQueue; + +/*! queue-microtask. MIT License. Feross Aboukhadijeh */ + +let promise; + +var queueMicrotask_1 = typeof queueMicrotask === 'function' + ? queueMicrotask.bind(typeof window !== 'undefined' ? window : commonjsGlobal) + // reuse resolved promise, and allocate it lazily + : cb => (promise || (promise = Promise.resolve())) + .then(cb) + .catch(err => setTimeout(() => { throw err }, 0)); + +var Producer$1 = {}; + +Object.defineProperty(Producer$1, "__esModule", { value: true }); +Producer$1.Producer = void 0; +const Logger_1$h = Logger$3; +const EnhancedEventEmitter_1$6 = EnhancedEventEmitter$1; +const errors_1$c = errors; +const logger$h = new Logger_1$h.Logger('Producer'); +class Producer extends EnhancedEventEmitter_1$6.EnhancedEventEmitter { + constructor({ id, localId, rtpSender, track, rtpParameters, stopTracks, disableTrackOnPause, zeroRtpOnPause, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1$6.EnhancedEventEmitter(); + logger$h.debug('constructor()'); + this._id = id; + this._localId = localId; + this._rtpSender = rtpSender; + this._track = track; + this._kind = track.kind; + this._rtpParameters = rtpParameters; + this._paused = disableTrackOnPause ? !track.enabled : false; + this._maxSpatialLayer = undefined; + this._stopTracks = stopTracks; + this._disableTrackOnPause = disableTrackOnPause; + this._zeroRtpOnPause = zeroRtpOnPause; + this._appData = appData || {}; + this.onTrackEnded = this.onTrackEnded.bind(this); + // NOTE: Minor issue. If zeroRtpOnPause is true, we cannot emit the + // '@replacetrack' event here, so RTCRtpSender.track won't be null. + this.handleTrack(); + } + /** + * Producer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Whether the Producer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._kind; + } + /** + * Associated RTCRtpSender. + */ + get rtpSender() { + return this._rtpSender; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Producer is paused. + */ + get paused() { + return this._paused; + } + /** + * Max spatial layer. + * + * @type {Number | undefined} + */ + get maxSpatialLayer() { + return this._maxSpatialLayer; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * App custom data setter. + */ + set appData(appData) { + this._appData = appData; + } + get observer() { + return this._observer; + } + /** + * Closes the Producer. + */ + close() { + if (this._closed) { + return; + } + logger$h.debug('close()'); + this._closed = true; + this.destroyTrack(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger$h.debug('transportClosed()'); + this._closed = true; + this.destroyTrack(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated RTCRtpSender stats. + */ + async getStats() { + if (this._closed) { + throw new errors_1$c.InvalidStateError('closed'); + } + return new Promise((resolve, reject) => { + this.safeEmit('@getstats', resolve, reject); + }); + } + /** + * Pauses sending media. + */ + pause() { + logger$h.debug('pause()'); + if (this._closed) { + logger$h.error('pause() | Producer closed'); + return; + } + this._paused = true; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = false; + } + if (this._zeroRtpOnPause) { + new Promise((resolve, reject) => { + this.safeEmit('@pause', resolve, reject); + }).catch(() => { }); + } + // Emit observer event. + this._observer.safeEmit('pause'); + } + /** + * Resumes sending media. + */ + resume() { + logger$h.debug('resume()'); + if (this._closed) { + logger$h.error('resume() | Producer closed'); + return; + } + this._paused = false; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = true; + } + if (this._zeroRtpOnPause) { + new Promise((resolve, reject) => { + this.safeEmit('@resume', resolve, reject); + }).catch(() => { }); + } + // Emit observer event. + this._observer.safeEmit('resume'); + } + /** + * Replaces the current track with a new one or null. + */ + async replaceTrack({ track }) { + logger$h.debug('replaceTrack() [track:%o]', track); + if (this._closed) { + // This must be done here. Otherwise there is no chance to stop the given + // track. + if (track && this._stopTracks) { + try { + track.stop(); + } + catch (error) { } + } + throw new errors_1$c.InvalidStateError('closed'); + } + else if (track && track.readyState === 'ended') { + throw new errors_1$c.InvalidStateError('track ended'); + } + // Do nothing if this is the same track as the current handled one. + if (track === this._track) { + logger$h.debug('replaceTrack() | same track, ignored'); + return; + } + await new Promise((resolve, reject) => { + this.safeEmit('@replacetrack', track, resolve, reject); + }); + // Destroy the previous track. + this.destroyTrack(); + // Set the new track. + this._track = track; + // If this Producer was paused/resumed and the state of the new + // track does not match, fix it. + if (this._track && this._disableTrackOnPause) { + if (!this._paused) { + this._track.enabled = true; + } + else if (this._paused) { + this._track.enabled = false; + } + } + // Handle the effective track. + this.handleTrack(); + } + /** + * Sets the video max spatial layer to be sent. + */ + async setMaxSpatialLayer(spatialLayer) { + if (this._closed) { + throw new errors_1$c.InvalidStateError('closed'); + } + else if (this._kind !== 'video') { + throw new errors_1$c.UnsupportedError('not a video Producer'); + } + else if (typeof spatialLayer !== 'number') { + throw new TypeError('invalid spatialLayer'); + } + if (spatialLayer === this._maxSpatialLayer) { + return; + } + await new Promise((resolve, reject) => { + this.safeEmit('@setmaxspatiallayer', spatialLayer, resolve, reject); + }).catch(() => { }); + this._maxSpatialLayer = spatialLayer; + } + async setRtpEncodingParameters(params) { + if (this._closed) { + throw new errors_1$c.InvalidStateError('closed'); + } + else if (typeof params !== 'object') { + throw new TypeError('invalid params'); + } + await new Promise((resolve, reject) => { + this.safeEmit('@setrtpencodingparameters', params, resolve, reject); + }); + } + onTrackEnded() { + logger$h.debug('track "ended" event'); + this.safeEmit('trackended'); + // Emit observer event. + this._observer.safeEmit('trackended'); + } + handleTrack() { + if (!this._track) { + return; + } + this._track.addEventListener('ended', this.onTrackEnded); + } + destroyTrack() { + if (!this._track) { + return; + } + try { + this._track.removeEventListener('ended', this.onTrackEnded); + // Just stop the track unless the app set stopTracks: false. + if (this._stopTracks) { + this._track.stop(); + } + } + catch (error) { } + } +} +Producer$1.Producer = Producer; + +var Consumer$1 = {}; + +Object.defineProperty(Consumer$1, "__esModule", { value: true }); +Consumer$1.Consumer = void 0; +const Logger_1$g = Logger$3; +const EnhancedEventEmitter_1$5 = EnhancedEventEmitter$1; +const errors_1$b = errors; +const logger$g = new Logger_1$g.Logger('Consumer'); +class Consumer extends EnhancedEventEmitter_1$5.EnhancedEventEmitter { + constructor({ id, localId, producerId, rtpReceiver, track, rtpParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1$5.EnhancedEventEmitter(); + logger$g.debug('constructor()'); + this._id = id; + this._localId = localId; + this._producerId = producerId; + this._rtpReceiver = rtpReceiver; + this._track = track; + this._rtpParameters = rtpParameters; + this._paused = !track.enabled; + this._appData = appData || {}; + this.onTrackEnded = this.onTrackEnded.bind(this); + this.handleTrack(); + } + /** + * Consumer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Associated Producer id. + */ + get producerId() { + return this._producerId; + } + /** + * Whether the Consumer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._track.kind; + } + /** + * Associated RTCRtpReceiver. + */ + get rtpReceiver() { + return this._rtpReceiver; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Consumer is paused. + */ + get paused() { + return this._paused; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * App custom data setter. + */ + set appData(appData) { + this._appData = appData; + } + get observer() { + return this._observer; + } + /** + * Closes the Consumer. + */ + close() { + if (this._closed) { + return; + } + logger$g.debug('close()'); + this._closed = true; + this.destroyTrack(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger$g.debug('transportClosed()'); + this._closed = true; + this.destroyTrack(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated RTCRtpReceiver stats. + */ + async getStats() { + if (this._closed) { + throw new errors_1$b.InvalidStateError('closed'); + } + return new Promise((resolve, reject) => { + this.safeEmit('@getstats', resolve, reject); + }); + } + /** + * Pauses receiving media. + */ + pause() { + logger$g.debug('pause()'); + if (this._closed) { + logger$g.error('pause() | Consumer closed'); + return; + } + if (this._paused) { + logger$g.debug('pause() | Consumer is already paused'); + return; + } + this._paused = true; + this._track.enabled = false; + this.emit('@pause'); + // Emit observer event. + this._observer.safeEmit('pause'); + } + /** + * Resumes receiving media. + */ + resume() { + logger$g.debug('resume()'); + if (this._closed) { + logger$g.error('resume() | Consumer closed'); + return; + } + if (!this._paused) { + logger$g.debug('resume() | Consumer is already resumed'); + return; + } + this._paused = false; + this._track.enabled = true; + this.emit('@resume'); + // Emit observer event. + this._observer.safeEmit('resume'); + } + onTrackEnded() { + logger$g.debug('track "ended" event'); + this.safeEmit('trackended'); + // Emit observer event. + this._observer.safeEmit('trackended'); + } + handleTrack() { + this._track.addEventListener('ended', this.onTrackEnded); + } + destroyTrack() { + try { + this._track.removeEventListener('ended', this.onTrackEnded); + this._track.stop(); + } + catch (error) { } + } +} +Consumer$1.Consumer = Consumer; + +var DataProducer$1 = {}; + +Object.defineProperty(DataProducer$1, "__esModule", { value: true }); +DataProducer$1.DataProducer = void 0; +const Logger_1$f = Logger$3; +const EnhancedEventEmitter_1$4 = EnhancedEventEmitter$1; +const errors_1$a = errors; +const logger$f = new Logger_1$f.Logger('DataProducer'); +class DataProducer extends EnhancedEventEmitter_1$4.EnhancedEventEmitter { + constructor({ id, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1$4.EnhancedEventEmitter(); + logger$f.debug('constructor()'); + this._id = id; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData || {}; + this.handleDataChannel(); + } + /** + * DataProducer id. + */ + get id() { + return this._id; + } + /** + * Whether the DataProducer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel bufferedAmount. + */ + get bufferedAmount() { + return this._dataChannel.bufferedAmount; + } + /** + * DataChannel bufferedAmountLowThreshold. + */ + get bufferedAmountLowThreshold() { + return this._dataChannel.bufferedAmountLowThreshold; + } + /** + * Set DataChannel bufferedAmountLowThreshold. + */ + set bufferedAmountLowThreshold(bufferedAmountLowThreshold) { + this._dataChannel.bufferedAmountLowThreshold = bufferedAmountLowThreshold; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * App custom data setter. + */ + set appData(appData) { + this._appData = appData; + } + get observer() { + return this._observer; + } + /** + * Closes the DataProducer. + */ + close() { + if (this._closed) { + return; + } + logger$f.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger$f.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Send a message. + * + * @param {String|Blob|ArrayBuffer|ArrayBufferView} data. + */ + send(data) { + logger$f.debug('send()'); + if (this._closed) { + throw new errors_1$a.InvalidStateError('closed'); + } + this._dataChannel.send(data); + } + handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) { + return; + } + logger$f.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) { + return; + } + let { error } = event; + if (!error) { + error = new Error('unknown DataChannel error'); + } + if (error.errorDetail === 'sctp-failure') { + logger$f.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); + } + else { + logger$f.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) { + return; + } + logger$f.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + // Emit observer event. + this._observer.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', () => { + if (this._closed) { + return; + } + logger$f.warn('DataChannel "message" event in a DataProducer, message discarded'); + }); + this._dataChannel.addEventListener('bufferedamountlow', () => { + if (this._closed) { + return; + } + this.safeEmit('bufferedamountlow'); + }); + } +} +DataProducer$1.DataProducer = DataProducer; + +var DataConsumer$1 = {}; + +Object.defineProperty(DataConsumer$1, "__esModule", { value: true }); +DataConsumer$1.DataConsumer = void 0; +const Logger_1$e = Logger$3; +const EnhancedEventEmitter_1$3 = EnhancedEventEmitter$1; +const logger$e = new Logger_1$e.Logger('DataConsumer'); +class DataConsumer extends EnhancedEventEmitter_1$3.EnhancedEventEmitter { + constructor({ id, dataProducerId, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1$3.EnhancedEventEmitter(); + logger$e.debug('constructor()'); + this._id = id; + this._dataProducerId = dataProducerId; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData || {}; + this.handleDataChannel(); + } + /** + * DataConsumer id. + */ + get id() { + return this._id; + } + /** + * Associated DataProducer id. + */ + get dataProducerId() { + return this._dataProducerId; + } + /** + * Whether the DataConsumer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel binaryType. + */ + get binaryType() { + return this._dataChannel.binaryType; + } + /** + * Set DataChannel binaryType. + */ + set binaryType(binaryType) { + this._dataChannel.binaryType = binaryType; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * App custom data setter. + */ + set appData(appData) { + this._appData = appData; + } + get observer() { + return this._observer; + } + /** + * Closes the DataConsumer. + */ + close() { + if (this._closed) { + return; + } + logger$e.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger$e.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) { + return; + } + logger$e.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) { + return; + } + let { error } = event; + if (!error) { + error = new Error('unknown DataChannel error'); + } + if (error.errorDetail === 'sctp-failure') { + logger$e.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); + } + else { + logger$e.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) { + return; + } + logger$e.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + // Emit observer event. + this._observer.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', (event) => { + if (this._closed) { + return; + } + this.safeEmit('message', event.data); + }); + } +} +DataConsumer$1.DataConsumer = DataConsumer; + +var __createBinding$g = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$g = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$g = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$g(result, mod, k); + __setModuleDefault$g(result, mod); + return result; +}; +var __importDefault = (commonjsGlobal && commonjsGlobal.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(Transport$1, "__esModule", { value: true }); +Transport$1.Transport = void 0; +const awaitqueue_1 = lib$1; +const queue_microtask_1 = __importDefault(queueMicrotask_1); +const Logger_1$d = Logger$3; +const EnhancedEventEmitter_1$2 = EnhancedEventEmitter$1; +const errors_1$9 = errors; +const utils$f = __importStar$g(utils$h); +const ortc$c = __importStar$g(ortc$d); +const Producer_1 = Producer$1; +const Consumer_1 = Consumer$1; +const DataProducer_1 = DataProducer$1; +const DataConsumer_1 = DataConsumer$1; +const logger$d = new Logger_1$d.Logger('Transport'); +class ConsumerCreationTask { + constructor(consumerOptions) { + this.consumerOptions = consumerOptions; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} +class Transport extends EnhancedEventEmitter_1$2.EnhancedEventEmitter { + constructor({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData, handlerFactory, extendedRtpCapabilities, canProduceByKind }) { + super(); + // Closed flag. + this._closed = false; + // Transport ICE gathering state. + this._iceGatheringState = 'new'; + // Transport connection state. + this._connectionState = 'new'; + // Map of Producers indexed by id. + this._producers = new Map(); + // Map of Consumers indexed by id. + this._consumers = new Map(); + // Map of DataProducers indexed by id. + this._dataProducers = new Map(); + // Map of DataConsumers indexed by id. + this._dataConsumers = new Map(); + // Whether the Consumer for RTP probation has been created. + this._probatorConsumerCreated = false; + // AwaitQueue instance to make async tasks happen sequentially. + this._awaitQueue = new awaitqueue_1.AwaitQueue(); + // Consumer creation tasks awaiting to be processed. + this._pendingConsumerTasks = []; + // Consumer creation in progress flag. + this._consumerCreationInProgress = false; + // Consumers pending to be paused. + this._pendingPauseConsumers = new Map(); + // Consumer pause in progress flag. + this._consumerPauseInProgress = false; + // Consumers pending to be resumed. + this._pendingResumeConsumers = new Map(); + // Consumer resume in progress flag. + this._consumerResumeInProgress = false; + // Consumers pending to be closed. + this._pendingCloseConsumers = new Map(); + // Consumer close in progress flag. + this._consumerCloseInProgress = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1$2.EnhancedEventEmitter(); + logger$d.debug('constructor() [id:%s, direction:%s]', id, direction); + this._id = id; + this._direction = direction; + this._extendedRtpCapabilities = extendedRtpCapabilities; + this._canProduceByKind = canProduceByKind; + this._maxSctpMessageSize = + sctpParameters ? sctpParameters.maxMessageSize : null; + // Clone and sanitize additionalSettings. + additionalSettings = utils$f.clone(additionalSettings) || {}; + delete additionalSettings.iceServers; + delete additionalSettings.iceTransportPolicy; + delete additionalSettings.bundlePolicy; + delete additionalSettings.rtcpMuxPolicy; + delete additionalSettings.sdpSemantics; + this._handler = handlerFactory(); + this._handler.run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities + }); + this._appData = appData || {}; + this.handleHandler(); + } + /** + * Transport id. + */ + get id() { + return this._id; + } + /** + * Whether the Transport is closed. + */ + get closed() { + return this._closed; + } + /** + * Transport direction. + */ + get direction() { + return this._direction; + } + /** + * RTC handler instance. + */ + get handler() { + return this._handler; + } + /** + * ICE gathering state. + */ + get iceGatheringState() { + return this._iceGatheringState; + } + /** + * Connection state. + */ + get connectionState() { + return this._connectionState; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * App custom data setter. + */ + set appData(appData) { + this._appData = appData; + } + get observer() { + return this._observer; + } + /** + * Close the Transport. + */ + close() { + if (this._closed) { + return; + } + logger$d.debug('close()'); + this._closed = true; + // Stop the AwaitQueue. + this._awaitQueue.stop(); + // Close the handler. + this._handler.close(); + // Change connection state to 'closed' since the handler may not emit + // '@connectionstatechange' event. + this._connectionState = 'closed'; + // Close all Producers. + for (const producer of this._producers.values()) { + producer.transportClosed(); + } + this._producers.clear(); + // Close all Consumers. + for (const consumer of this._consumers.values()) { + consumer.transportClosed(); + } + this._consumers.clear(); + // Close all DataProducers. + for (const dataProducer of this._dataProducers.values()) { + dataProducer.transportClosed(); + } + this._dataProducers.clear(); + // Close all DataConsumers. + for (const dataConsumer of this._dataConsumers.values()) { + dataConsumer.transportClosed(); + } + this._dataConsumers.clear(); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated Transport (RTCPeerConnection) stats. + * + * @returns {RTCStatsReport} + */ + async getStats() { + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + return this._handler.getTransportStats(); + } + /** + * Restart ICE connection. + */ + async restartIce({ iceParameters }) { + logger$d.debug('restartIce()'); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (!iceParameters) { + throw new TypeError('missing iceParameters'); + } + // Enqueue command. + return this._awaitQueue.push(async () => await this._handler.restartIce(iceParameters), 'transport.restartIce()'); + } + /** + * Update ICE servers. + */ + async updateIceServers({ iceServers } = {}) { + logger$d.debug('updateIceServers()'); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (!Array.isArray(iceServers)) { + throw new TypeError('missing iceServers'); + } + // Enqueue command. + return this._awaitQueue.push(async () => this._handler.updateIceServers(iceServers), 'transport.updateIceServers()'); + } + /** + * Create a Producer. + */ + async produce({ track, encodings, codecOptions, codec, stopTracks = true, disableTrackOnPause = true, zeroRtpOnPause = false, appData = {} } = {}) { + logger$d.debug('produce() [track:%o]', track); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (!track) { + throw new TypeError('missing track'); + } + else if (this._direction !== 'send') { + throw new errors_1$9.UnsupportedError('not a sending Transport'); + } + else if (!this._canProduceByKind[track.kind]) { + throw new errors_1$9.UnsupportedError(`cannot produce ${track.kind}`); + } + else if (track.readyState === 'ended') { + throw new errors_1$9.InvalidStateError('track ended'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (this.listenerCount('produce') === 0) { + throw new TypeError('no "produce" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Enqueue command. + return this._awaitQueue.push(async () => { + let normalizedEncodings; + if (encodings && !Array.isArray(encodings)) { + throw TypeError('encodings must be an array'); + } + else if (encodings && encodings.length === 0) { + normalizedEncodings = undefined; + } + else if (encodings) { + normalizedEncodings = encodings + .map((encoding) => { + const normalizedEncoding = { active: true }; + if (encoding.active === false) { + normalizedEncoding.active = false; + } + if (typeof encoding.dtx === 'boolean') { + normalizedEncoding.dtx = encoding.dtx; + } + if (typeof encoding.scalabilityMode === 'string') { + normalizedEncoding.scalabilityMode = encoding.scalabilityMode; + } + if (typeof encoding.scaleResolutionDownBy === 'number') { + normalizedEncoding.scaleResolutionDownBy = encoding.scaleResolutionDownBy; + } + if (typeof encoding.maxBitrate === 'number') { + normalizedEncoding.maxBitrate = encoding.maxBitrate; + } + if (typeof encoding.maxFramerate === 'number') { + normalizedEncoding.maxFramerate = encoding.maxFramerate; + } + if (typeof encoding.adaptivePtime === 'boolean') { + normalizedEncoding.adaptivePtime = encoding.adaptivePtime; + } + if (typeof encoding.priority === 'string') { + normalizedEncoding.priority = encoding.priority; + } + if (typeof encoding.networkPriority === 'string') { + normalizedEncoding.networkPriority = encoding.networkPriority; + } + return normalizedEncoding; + }); + } + const { localId, rtpParameters, rtpSender } = await this._handler.send({ + track, + encodings: normalizedEncodings, + codecOptions, + codec + }); + try { + // This will fill rtpParameters's missing fields with default values. + ortc$c.validateRtpParameters(rtpParameters); + const { id } = await new Promise((resolve, reject) => { + this.safeEmit('produce', { + kind: track.kind, + rtpParameters, + appData + }, resolve, reject); + }); + const producer = new Producer_1.Producer({ + id, + localId, + rtpSender, + track, + rtpParameters, + stopTracks, + disableTrackOnPause, + zeroRtpOnPause, + appData + }); + this._producers.set(producer.id, producer); + this.handleProducer(producer); + // Emit observer event. + this._observer.safeEmit('newproducer', producer); + return producer; + } + catch (error) { + this._handler.stopSending(localId) + .catch(() => { }); + throw error; + } + }, 'transport.produce()') + // This catch is needed to stop the given track if the command above + // failed due to closed Transport. + .catch((error) => { + if (stopTracks) { + try { + track.stop(); + } + catch (error2) { } + } + throw error; + }); + } + /** + * Create a Consumer to consume a remote Producer. + */ + async consume({ id, producerId, kind, rtpParameters, streamId, appData = {} }) { + logger$d.debug('consume()'); + rtpParameters = utils$f.clone(rtpParameters); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (this._direction !== 'recv') { + throw new errors_1$9.UnsupportedError('not a receiving Transport'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof producerId !== 'string') { + throw new TypeError('missing producerId'); + } + else if (kind !== 'audio' && kind !== 'video') { + throw new TypeError(`invalid kind '${kind}'`); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Ensure the device can consume it. + const canConsume = ortc$c.canReceive(rtpParameters, this._extendedRtpCapabilities); + if (!canConsume) { + throw new errors_1$9.UnsupportedError('cannot consume this Producer'); + } + const consumerCreationTask = new ConsumerCreationTask({ + id, + producerId, + kind, + rtpParameters, + streamId, + appData + }); + // Store the Consumer creation task. + this._pendingConsumerTasks.push(consumerCreationTask); + // There is no Consumer creation in progress, create it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + return; + } + if (this._consumerCreationInProgress === false) { + this.createPendingConsumers(); + } + }); + return consumerCreationTask.promise; + } + /** + * Create a DataProducer + */ + async produceData({ ordered = true, maxPacketLifeTime, maxRetransmits, label = '', protocol = '', appData = {} } = {}) { + logger$d.debug('produceData()'); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (this._direction !== 'send') { + throw new errors_1$9.UnsupportedError('not a sending Transport'); + } + else if (!this._maxSctpMessageSize) { + throw new errors_1$9.UnsupportedError('SCTP not enabled by remote Transport'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (this.listenerCount('producedata') === 0) { + throw new TypeError('no "producedata" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + if (maxPacketLifeTime || maxRetransmits) { + ordered = false; + } + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel, sctpStreamParameters } = await this._handler.sendDataChannel({ + ordered, + maxPacketLifeTime, + maxRetransmits, + label, + protocol + }); + // This will fill sctpStreamParameters's missing fields with default values. + ortc$c.validateSctpStreamParameters(sctpStreamParameters); + const { id } = await new Promise((resolve, reject) => { + this.safeEmit('producedata', { + sctpStreamParameters, + label, + protocol, + appData + }, resolve, reject); + }); + const dataProducer = new DataProducer_1.DataProducer({ + id, + dataChannel, + sctpStreamParameters, + appData + }); + this._dataProducers.set(dataProducer.id, dataProducer); + this.handleDataProducer(dataProducer); + // Emit observer event. + this._observer.safeEmit('newdataproducer', dataProducer); + return dataProducer; + }, 'transport.produceData()'); + } + /** + * Create a DataConsumer + */ + async consumeData({ id, dataProducerId, sctpStreamParameters, label = '', protocol = '', appData = {} }) { + logger$d.debug('consumeData()'); + sctpStreamParameters = utils$f.clone(sctpStreamParameters); + if (this._closed) { + throw new errors_1$9.InvalidStateError('closed'); + } + else if (this._direction !== 'recv') { + throw new errors_1$9.UnsupportedError('not a receiving Transport'); + } + else if (!this._maxSctpMessageSize) { + throw new errors_1$9.UnsupportedError('SCTP not enabled by remote Transport'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof dataProducerId !== 'string') { + throw new TypeError('missing dataProducerId'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // This may throw. + ortc$c.validateSctpStreamParameters(sctpStreamParameters); + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel } = await this._handler.receiveDataChannel({ + sctpStreamParameters, + label, + protocol + }); + const dataConsumer = new DataConsumer_1.DataConsumer({ + id, + dataProducerId, + dataChannel, + sctpStreamParameters, + appData + }); + this._dataConsumers.set(dataConsumer.id, dataConsumer); + this.handleDataConsumer(dataConsumer); + // Emit observer event. + this._observer.safeEmit('newdataconsumer', dataConsumer); + return dataConsumer; + }, 'transport.consumeData()'); + } + // This method is guaranteed to never throw. + async createPendingConsumers() { + this._consumerCreationInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingConsumerTasks.length === 0) { + logger$d.debug('createPendingConsumers() | there is no Consumer to be created'); + return; + } + const pendingConsumerTasks = [...this._pendingConsumerTasks]; + // Clear pending Consumer tasks. + this._pendingConsumerTasks = []; + // Video Consumer in order to create the probator. + let videoConsumerForProbator = undefined; + // Fill options list. + const optionsList = []; + for (const task of pendingConsumerTasks) { + const { id, kind, rtpParameters, streamId } = task.consumerOptions; + optionsList.push({ + trackId: id, + kind: kind, + rtpParameters, + streamId + }); + } + try { + const results = await this._handler.receive(optionsList); + for (let idx = 0; idx < results.length; ++idx) { + const task = pendingConsumerTasks[idx]; + const result = results[idx]; + const { id, producerId, kind, rtpParameters, appData } = task.consumerOptions; + const { localId, rtpReceiver, track } = result; + const consumer = new Consumer_1.Consumer({ + id: id, + localId, + producerId: producerId, + rtpReceiver, + track, + rtpParameters, + appData: appData + }); + this._consumers.set(consumer.id, consumer); + this.handleConsumer(consumer); + // If this is the first video Consumer and the Consumer for RTP probation + // has not yet been created, it's time to create it. + if (!this._probatorConsumerCreated && + !videoConsumerForProbator && kind === 'video') { + videoConsumerForProbator = consumer; + } + // Emit observer event. + this._observer.safeEmit('newconsumer', consumer); + task.resolve(consumer); + } + } + catch (error) { + for (const task of pendingConsumerTasks) { + task.reject(error); + } + } + // If RTP probation must be handled, do it now. + if (videoConsumerForProbator) { + try { + const probatorRtpParameters = ortc$c.generateProbatorRtpParameters(videoConsumerForProbator.rtpParameters); + await this._handler.receive([{ + trackId: 'probator', + kind: 'video', + rtpParameters: probatorRtpParameters + }]); + logger$d.debug('createPendingConsumers() | Consumer for RTP probation created'); + this._probatorConsumerCreated = true; + } + catch (error) { + logger$d.error('createPendingConsumers() | failed to create Consumer for RTP probation:%o', error); + } + } + }, 'transport.createPendingConsumers()') + .then(() => { + this._consumerCreationInProgress = false; + // There are pending Consumer tasks, enqueue their creation. + if (this._pendingConsumerTasks.length > 0) { + this.createPendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + pausePendingConsumers() { + this._consumerPauseInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingPauseConsumers.size === 0) { + logger$d.debug('pausePendingConsumers() | there is no Consumer to be paused'); + return; + } + const pendingPauseConsumers = Array.from(this._pendingPauseConsumers.values()); + // Clear pending pause Consumer map. + this._pendingPauseConsumers.clear(); + try { + const localIds = pendingPauseConsumers + .map((consumer) => consumer.localId); + await this._handler.pauseReceiving(localIds); + } + catch (error) { + logger$d.error('pausePendingConsumers() | failed to pause Consumers:', error); + } + }, 'transport.pausePendingConsumers') + .then(() => { + this._consumerPauseInProgress = false; + // There are pending Consumers to be paused, do it. + if (this._pendingPauseConsumers.size > 0) { + this.pausePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + resumePendingConsumers() { + this._consumerResumeInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingResumeConsumers.size === 0) { + logger$d.debug('resumePendingConsumers() | there is no Consumer to be resumed'); + return; + } + const pendingResumeConsumers = Array.from(this._pendingResumeConsumers.values()); + // Clear pending resume Consumer map. + this._pendingResumeConsumers.clear(); + try { + const localIds = pendingResumeConsumers + .map((consumer) => consumer.localId); + await this._handler.resumeReceiving(localIds); + } + catch (error) { + logger$d.error('resumePendingConsumers() | failed to resume Consumers:', error); + } + }, 'transport.resumePendingConsumers') + .then(() => { + this._consumerResumeInProgress = false; + // There are pending Consumer to be resumed, do it. + if (this._pendingResumeConsumers.size > 0) { + this.resumePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + closePendingConsumers() { + this._consumerCloseInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingCloseConsumers.size === 0) { + logger$d.debug('closePendingConsumers() | there is no Consumer to be closed'); + return; + } + const pendingCloseConsumers = Array.from(this._pendingCloseConsumers.values()); + // Clear pending close Consumer map. + this._pendingCloseConsumers.clear(); + try { + await this._handler.stopReceiving(pendingCloseConsumers.map((consumer) => consumer.localId)); + } + catch (error) { + logger$d.error('closePendingConsumers() | failed to close Consumers:', error); + } + }, 'transport.closePendingConsumers') + .then(() => { + this._consumerCloseInProgress = false; + // There are pending Consumer to be resumed, do it. + if (this._pendingCloseConsumers.size > 0) { + this.closePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + handleHandler() { + const handler = this._handler; + handler.on('@connect', ({ dtlsParameters }, callback, errback) => { + if (this._closed) { + errback(new errors_1$9.InvalidStateError('closed')); + return; + } + this.safeEmit('connect', { dtlsParameters }, callback, errback); + }); + handler.on('@icegatheringstatechange', (iceGatheringState) => { + if (iceGatheringState === this._iceGatheringState) { + return; + } + logger$d.debug('ICE gathering state changed to %s', iceGatheringState); + this._iceGatheringState = iceGatheringState; + if (!this._closed) { + this.safeEmit('icegatheringstatechange', iceGatheringState); + } + }); + handler.on('@connectionstatechange', (connectionState) => { + if (connectionState === this._connectionState) { + return; + } + logger$d.debug('connection state changed to %s', connectionState); + this._connectionState = connectionState; + if (!this._closed) { + this.safeEmit('connectionstatechange', connectionState); + } + }); + } + handleProducer(producer) { + producer.on('@close', () => { + this._producers.delete(producer.id); + if (this._closed) { + return; + } + this._awaitQueue.push(async () => await this._handler.stopSending(producer.localId), 'producer @close event') + .catch((error) => logger$d.warn('producer.close() failed:%o', error)); + }); + producer.on('@pause', (callback, errback) => { + this._awaitQueue.push(async () => await this._handler.pauseSending(producer.localId), 'producer @pause event') + .then(callback) + .catch(errback); + }); + producer.on('@resume', (callback, errback) => { + this._awaitQueue.push(async () => await this._handler.resumeSending(producer.localId), 'producer @resume event') + .then(callback) + .catch(errback); + }); + producer.on('@replacetrack', (track, callback, errback) => { + this._awaitQueue.push(async () => await this._handler.replaceTrack(producer.localId, track), 'producer @replacetrack event') + .then(callback) + .catch(errback); + }); + producer.on('@setmaxspatiallayer', (spatialLayer, callback, errback) => { + this._awaitQueue.push(async () => (await this._handler.setMaxSpatialLayer(producer.localId, spatialLayer)), 'producer @setmaxspatiallayer event') + .then(callback) + .catch(errback); + }); + producer.on('@setrtpencodingparameters', (params, callback, errback) => { + this._awaitQueue.push(async () => (await this._handler.setRtpEncodingParameters(producer.localId, params)), 'producer @setrtpencodingparameters event') + .then(callback) + .catch(errback); + }); + producer.on('@getstats', (callback, errback) => { + if (this._closed) { + return errback(new errors_1$9.InvalidStateError('closed')); + } + this._handler.getSenderStats(producer.localId) + .then(callback) + .catch(errback); + }); + } + handleConsumer(consumer) { + consumer.on('@close', () => { + this._consumers.delete(consumer.id); + this._pendingPauseConsumers.delete(consumer.id); + this._pendingResumeConsumers.delete(consumer.id); + if (this._closed) { + return; + } + // Store the Consumer into the close list. + this._pendingCloseConsumers.set(consumer.id, consumer); + // There is no Consumer close in progress, do it now. + if (this._consumerCloseInProgress === false) { + this.closePendingConsumers(); + } + }); + consumer.on('@pause', () => { + // If Consumer is pending to be resumed, remove from pending resume list. + if (this._pendingResumeConsumers.has(consumer.id)) { + this._pendingResumeConsumers.delete(consumer.id); + } + // Store the Consumer into the pending list. + this._pendingPauseConsumers.set(consumer.id, consumer); + // There is no Consumer pause in progress, do it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + return; + } + if (this._consumerPauseInProgress === false) { + this.pausePendingConsumers(); + } + }); + }); + consumer.on('@resume', () => { + // If Consumer is pending to be paused, remove from pending pause list. + if (this._pendingPauseConsumers.has(consumer.id)) { + this._pendingPauseConsumers.delete(consumer.id); + } + // Store the Consumer into the pending list. + this._pendingResumeConsumers.set(consumer.id, consumer); + // There is no Consumer resume in progress, do it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + return; + } + if (this._consumerResumeInProgress === false) { + this.resumePendingConsumers(); + } + }); + }); + consumer.on('@getstats', (callback, errback) => { + if (this._closed) { + return errback(new errors_1$9.InvalidStateError('closed')); + } + this._handler.getReceiverStats(consumer.localId) + .then(callback) + .catch(errback); + }); + } + handleDataProducer(dataProducer) { + dataProducer.on('@close', () => { + this._dataProducers.delete(dataProducer.id); + }); + } + handleDataConsumer(dataConsumer) { + dataConsumer.on('@close', () => { + this._dataConsumers.delete(dataConsumer.id); + }); + } +} +Transport$1.Transport = Transport; + +var Chrome111$1 = {}; + +var lib = {}; + +var parser$1 = {}; + +var grammar$2 = {exports: {}}; + +var grammar$1 = grammar$2.exports = { + v: [{ + name: 'version', + reg: /^(\d*)$/ + }], + o: [{ + // o=- 20518 0 IN IP4 203.0.113.1 + // NB: sessionId will be a String in most cases because it is huge + name: 'origin', + reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, + names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], + format: '%s %s %d %s IP%d %s' + }], + // default parsing of these only (though some of these feel outdated) + s: [{ name: 'name' }], + i: [{ name: 'description' }], + u: [{ name: 'uri' }], + e: [{ name: 'email' }], + p: [{ name: 'phone' }], + z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly... + r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly + // k: [{}], // outdated thing ignored + t: [{ + // t=0 0 + name: 'timing', + reg: /^(\d*) (\d*)/, + names: ['start', 'stop'], + format: '%d %d' + }], + c: [{ + // c=IN IP4 10.47.197.26 + name: 'connection', + reg: /^IN IP(\d) (\S*)/, + names: ['version', 'ip'], + format: 'IN IP%d %s' + }], + b: [{ + // b=AS:4000 + push: 'bandwidth', + reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, + names: ['type', 'limit'], + format: '%s:%s' + }], + m: [{ + // m=video 51744 RTP/AVP 126 97 98 34 31 + // NB: special - pushes to session + // TODO: rtp/fmtp should be filtered by the payloads found here? + reg: /^(\w*) (\d*) ([\w/]*)(?: (.*))?/, + names: ['type', 'port', 'protocol', 'payloads'], + format: '%s %d %s %s' + }], + a: [ + { + // a=rtpmap:110 opus/48000/2 + push: 'rtp', + reg: /^rtpmap:(\d*) ([\w\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/, + names: ['payload', 'codec', 'rate', 'encoding'], + format: function (o) { + return (o.encoding) + ? 'rtpmap:%d %s/%s/%s' + : o.rate + ? 'rtpmap:%d %s/%s' + : 'rtpmap:%d %s'; + } + }, + { + // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 + // a=fmtp:111 minptime=10; useinbandfec=1 + push: 'fmtp', + reg: /^fmtp:(\d*) ([\S| ]*)/, + names: ['payload', 'config'], + format: 'fmtp:%d %s' + }, + { + // a=control:streamid=0 + name: 'control', + reg: /^control:(.*)/, + format: 'control:%s' + }, + { + // a=rtcp:65179 IN IP4 193.84.77.194 + name: 'rtcp', + reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, + names: ['port', 'netType', 'ipVer', 'address'], + format: function (o) { + return (o.address != null) + ? 'rtcp:%d %s IP%d %s' + : 'rtcp:%d'; + } + }, + { + // a=rtcp-fb:98 trr-int 100 + push: 'rtcpFbTrrInt', + reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, + names: ['payload', 'value'], + format: 'rtcp-fb:%s trr-int %d' + }, + { + // a=rtcp-fb:98 nack rpsi + push: 'rtcpFb', + reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, + names: ['payload', 'type', 'subtype'], + format: function (o) { + return (o.subtype != null) + ? 'rtcp-fb:%s %s %s' + : 'rtcp-fb:%s %s'; + } + }, + { + // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + // a=extmap:1/recvonly URI-gps-string + // a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 + push: 'ext', + reg: /^extmap:(\d+)(?:\/(\w+))?(?: (urn:ietf:params:rtp-hdrext:encrypt))? (\S*)(?: (\S*))?/, + names: ['value', 'direction', 'encrypt-uri', 'uri', 'config'], + format: function (o) { + return ( + 'extmap:%d' + + (o.direction ? '/%s' : '%v') + + (o['encrypt-uri'] ? ' %s' : '%v') + + ' %s' + + (o.config ? ' %s' : '') + ); + } + }, + { + // a=extmap-allow-mixed + name: 'extmapAllowMixed', + reg: /^(extmap-allow-mixed)/ + }, + { + // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 + push: 'crypto', + reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, + names: ['id', 'suite', 'config', 'sessionConfig'], + format: function (o) { + return (o.sessionConfig != null) + ? 'crypto:%d %s %s %s' + : 'crypto:%d %s %s'; + } + }, + { + // a=setup:actpass + name: 'setup', + reg: /^setup:(\w*)/, + format: 'setup:%s' + }, + { + // a=connection:new + name: 'connectionType', + reg: /^connection:(new|existing)/, + format: 'connection:%s' + }, + { + // a=mid:1 + name: 'mid', + reg: /^mid:([^\s]*)/, + format: 'mid:%s' + }, + { + // a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a + name: 'msid', + reg: /^msid:(.*)/, + format: 'msid:%s' + }, + { + // a=ptime:20 + name: 'ptime', + reg: /^ptime:(\d*(?:\.\d*)*)/, + format: 'ptime:%d' + }, + { + // a=maxptime:60 + name: 'maxptime', + reg: /^maxptime:(\d*(?:\.\d*)*)/, + format: 'maxptime:%d' + }, + { + // a=sendrecv + name: 'direction', + reg: /^(sendrecv|recvonly|sendonly|inactive)/ + }, + { + // a=ice-lite + name: 'icelite', + reg: /^(ice-lite)/ + }, + { + // a=ice-ufrag:F7gI + name: 'iceUfrag', + reg: /^ice-ufrag:(\S*)/, + format: 'ice-ufrag:%s' + }, + { + // a=ice-pwd:x9cml/YzichV2+XlhiMu8g + name: 'icePwd', + reg: /^ice-pwd:(\S*)/, + format: 'ice-pwd:%s' + }, + { + // a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 + name: 'fingerprint', + reg: /^fingerprint:(\S*) (\S*)/, + names: ['type', 'hash'], + format: 'fingerprint:%s %s' + }, + { + // a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host + // a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10 + // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10 + push:'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?/, + names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation', 'network-id', 'network-cost'], + format: function (o) { + var str = 'candidate:%s %d %s %d %s %d typ %s'; + + str += (o.raddr != null) ? ' raddr %s rport %d' : '%v%v'; + + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += (o.tcptype != null) ? ' tcptype %s' : '%v'; + + if (o.generation != null) { + str += ' generation %d'; + } + + str += (o['network-id'] != null) ? ' network-id %d' : '%v'; + str += (o['network-cost'] != null) ? ' network-cost %d' : '%v'; + return str; + } + }, + { + // a=end-of-candidates (keep after the candidates line for readability) + name: 'endOfCandidates', + reg: /^(end-of-candidates)/ + }, + { + // a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... + name: 'remoteCandidates', + reg: /^remote-candidates:(.*)/, + format: 'remote-candidates:%s' + }, + { + // a=ice-options:google-ice + name: 'iceOptions', + reg: /^ice-options:(\S*)/, + format: 'ice-options:%s' + }, + { + // a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 + push: 'ssrcs', + reg: /^ssrc:(\d*) ([\w_-]*)(?::(.*))?/, + names: ['id', 'attribute', 'value'], + format: function (o) { + var str = 'ssrc:%d'; + if (o.attribute != null) { + str += ' %s'; + if (o.value != null) { + str += ':%s'; + } + } + return str; + } + }, + { + // a=ssrc-group:FEC 1 2 + // a=ssrc-group:FEC-FR 3004364195 1080772241 + push: 'ssrcGroups', + // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E + reg: /^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\w]*) (.*)/, + names: ['semantics', 'ssrcs'], + format: 'ssrc-group:%s %s' + }, + { + // a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV + name: 'msidSemantic', + reg: /^msid-semantic:\s?(\w*) (\S*)/, + names: ['semantic', 'token'], + format: 'msid-semantic: %s %s' // space after ':' is not accidental + }, + { + // a=group:BUNDLE audio video + push: 'groups', + reg: /^group:(\w*) (.*)/, + names: ['type', 'mids'], + format: 'group:%s %s' + }, + { + // a=rtcp-mux + name: 'rtcpMux', + reg: /^(rtcp-mux)/ + }, + { + // a=rtcp-rsize + name: 'rtcpRsize', + reg: /^(rtcp-rsize)/ + }, + { + // a=sctpmap:5000 webrtc-datachannel 1024 + name: 'sctpmap', + reg: /^sctpmap:([\w_/]*) (\S*)(?: (\S*))?/, + names: ['sctpmapNumber', 'app', 'maxMessageSize'], + format: function (o) { + return (o.maxMessageSize != null) + ? 'sctpmap:%s %s %s' + : 'sctpmap:%s %s'; + } + }, + { + // a=x-google-flag:conference + name: 'xGoogleFlag', + reg: /^x-google-flag:([^\s]*)/, + format: 'x-google-flag:%s' + }, + { + // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0 + push: 'rids', + reg: /^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?/, + names: ['id', 'direction', 'params'], + format: function (o) { + return (o.params) ? 'rid:%s %s %s' : 'rid:%s %s'; + } + }, + { + // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250] + // a=imageattr:* send [x=800,y=640] recv * + // a=imageattr:100 recv [x=320,y=240] + push: 'imageattrs', + reg: new RegExp( + // a=imageattr:97 + '^imageattr:(\\d+|\\*)' + + // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] + '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' + + // recv [x=330,y=250] + '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?' + ), + names: ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'], + format: function (o) { + return 'imageattr:%s %s %s' + (o.dir2 ? ' %s %s' : ''); + } + }, + { + // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8 + // a=simulcast:recv 1;4,5 send 6;7 + name: 'simulcast', + reg: new RegExp( + // a=simulcast: + '^simulcast:' + + // send 1,2,3;~4,~5 + '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' + + // space + recv 6;~7,~8 + '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' + + // end + '$' + ), + names: ['dir1', 'list1', 'dir2', 'list2'], + format: function (o) { + return 'simulcast:%s %s' + (o.dir2 ? ' %s %s' : ''); + } + }, + { + // old simulcast draft 03 (implemented by Firefox) + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03 + // a=simulcast: recv pt=97;98 send pt=97 + // a=simulcast: send rid=5;6;7 paused=6,7 + name: 'simulcast_03', + reg: /^simulcast:[\s\t]+([\S+\s\t]+)$/, + names: ['value'], + format: 'simulcast: %s' + }, + { + // a=framerate:25 + // a=framerate:29.97 + name: 'framerate', + reg: /^framerate:(\d+(?:$|\.\d+))/, + format: 'framerate:%s' + }, + { + // RFC4570 + // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5 + name: 'sourceFilter', + reg: /^source-filter: *(excl|incl) (\S*) (IP4|IP6|\*) (\S*) (.*)/, + names: ['filterMode', 'netType', 'addressTypes', 'destAddress', 'srcList'], + format: 'source-filter: %s %s %s %s %s' + }, + { + // a=bundle-only + name: 'bundleOnly', + reg: /^(bundle-only)/ + }, + { + // a=label:1 + name: 'label', + reg: /^label:(.+)/, + format: 'label:%s' + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-5 + name: 'sctpPort', + reg: /^sctp-port:(\d+)$/, + format: 'sctp-port:%s' + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-6 + name: 'maxMessageSize', + reg: /^max-message-size:(\d+)$/, + format: 'max-message-size:%s' + }, + { + // RFC7273 + // a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 + push:'tsRefClocks', + reg: /^ts-refclk:([^\s=]*)(?:=(\S*))?/, + names: ['clksrc', 'clksrcExt'], + format: function (o) { + return 'ts-refclk:%s' + (o.clksrcExt != null ? '=%s' : ''); + } + }, + { + // RFC7273 + // a=mediaclk:direct=963214424 + name:'mediaClk', + reg: /^mediaclk:(?:id=(\S*))? *([^\s=]*)(?:=(\S*))?(?: *rate=(\d+)\/(\d+))?/, + names: ['id', 'mediaClockName', 'mediaClockValue', 'rateNumerator', 'rateDenominator'], + format: function (o) { + var str = 'mediaclk:'; + str += (o.id != null ? 'id=%s %s' : '%v%s'); + str += (o.mediaClockValue != null ? '=%s' : ''); + str += (o.rateNumerator != null ? ' rate=%s' : ''); + str += (o.rateDenominator != null ? '/%s' : ''); + return str; + } + }, + { + // a=keywds:keywords + name: 'keywords', + reg: /^keywds:(.+)$/, + format: 'keywds:%s' + }, + { + // a=content:main + name: 'content', + reg: /^content:(.+)/, + format: 'content:%s' + }, + // BFCP https://tools.ietf.org/html/rfc4583 + { + // a=floorctrl:c-s + name: 'bfcpFloorCtrl', + reg: /^floorctrl:(c-only|s-only|c-s)/, + format: 'floorctrl:%s' + }, + { + // a=confid:1 + name: 'bfcpConfId', + reg: /^confid:(\d+)/, + format: 'confid:%s' + }, + { + // a=userid:1 + name: 'bfcpUserId', + reg: /^userid:(\d+)/, + format: 'userid:%s' + }, + { + // a=floorid:1 + name: 'bfcpFloorId', + reg: /^floorid:(.+) (?:m-stream|mstrm):(.+)/, + names: ['id', 'mStream'], + format: 'floorid:%s mstrm:%s' + }, + { + // any a= that we don't understand is kept verbatim on media.invalid + push: 'invalid', + names: ['value'] + } + ] +}; + +// set sensible defaults to avoid polluting the grammar with boring details +Object.keys(grammar$1).forEach(function (key) { + var objs = grammar$1[key]; + objs.forEach(function (obj) { + if (!obj.reg) { + obj.reg = /(.*)/; + } + if (!obj.format) { + obj.format = '%s'; + } + }); +}); + +var grammarExports = grammar$2.exports; + +(function (exports) { + var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; + }; + + var attachProperties = function (match, location, names, rawName) { + if (rawName && !names) { + location[rawName] = toIntIfInt(match[1]); + } + else { + for (var i = 0; i < names.length; i += 1) { + if (match[i+1] != null) { + location[names[i]] = toIntIfInt(match[i+1]); + } + } + } + }; + + var parseReg = function (obj, location, content) { + var needsBlank = obj.name && obj.names; + if (obj.push && !location[obj.push]) { + location[obj.push] = []; + } + else if (needsBlank && !location[obj.name]) { + location[obj.name] = {}; + } + var keyLocation = obj.push ? + {} : // blank object that will be pushed + needsBlank ? location[obj.name] : location; // otherwise, named location or root + + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + + if (obj.push) { + location[obj.push].push(keyLocation); + } + }; + + var grammar = grammarExports; + var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + + exports.parse = function (sdp) { + var session = {} + , media = [] + , location = session; // points at where properties go under (one of the above) + + // parse lines we understand + sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) { + var type = l[0]; + var content = l.slice(2); + if (type === 'm') { + media.push({rtp: [], fmtp: []}); + location = media[media.length-1]; // point at latest media line + } + + for (var j = 0; j < (grammar[type] || []).length; j += 1) { + var obj = grammar[type][j]; + if (obj.reg.test(content)) { + return parseReg(obj, location, content); + } + } + }); + + session.media = media; // link it up + return session; + }; + + var paramReducer = function (acc, expr) { + var s = expr.split(/=(.+)/, 2); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } else if (s.length === 1 && expr.length > 1) { + acc[s[0]] = undefined; + } + return acc; + }; + + exports.parseParams = function (str) { + return str.split(/;\s?/).reduce(paramReducer, {}); + }; + + // For backward compatibility - alias will be removed in 3.0.0 + exports.parseFmtpConfig = exports.parseParams; + + exports.parsePayloads = function (str) { + return str.toString().split(' ').map(Number); + }; + + exports.parseRemoteCandidates = function (str) { + var candidates = []; + var parts = str.split(' ').map(toIntIfInt); + for (var i = 0; i < parts.length; i += 3) { + candidates.push({ + component: parts[i], + ip: parts[i + 1], + port: parts[i + 2] + }); + } + return candidates; + }; + + exports.parseImageAttributes = function (str) { + return str.split(' ').map(function (item) { + return item.substring(1, item.length-1).split(',').reduce(paramReducer, {}); + }); + }; + + exports.parseSimulcastStreamList = function (str) { + return str.split(';').map(function (stream) { + return stream.split(',').map(function (format) { + var scid, paused = false; + + if (format[0] !== '~') { + scid = toIntIfInt(format); + } else { + scid = toIntIfInt(format.substring(1, format.length)); + paused = true; + } + + return { + scid: scid, + paused: paused + }; + }); + }); + }; +} (parser$1)); + +var grammar = grammarExports; + +// customized util.format - discards excess arguments and can void middle ones +var formatRegExp = /%[sdv%]/g; +var format = function (formatStr) { + var i = 1; + var args = arguments; + var len = args.length; + return formatStr.replace(formatRegExp, function (x) { + if (i >= len) { + return x; // missing argument + } + var arg = args[i]; + i += 1; + switch (x) { + case '%%': + return '%'; + case '%s': + return String(arg); + case '%d': + return Number(arg); + case '%v': + return ''; + } + }); + // NB: we discard excess arguments - they are typically undefined from makeLine +}; + +var makeLine = function (type, obj, location) { + var str = obj.format instanceof Function ? + (obj.format(obj.push ? location : location[obj.name])) : + obj.format; + + var args = [type + '=' + str]; + if (obj.names) { + for (var i = 0; i < obj.names.length; i += 1) { + var n = obj.names[i]; + if (obj.name) { + args.push(location[obj.name][n]); + } + else { // for mLine and push attributes + args.push(location[obj.names[i]]); + } + } + } + else { + args.push(location[obj.name]); + } + return format.apply(null, args); +}; + +// RFC specified order +// TODO: extend this with all the rest +var defaultOuterOrder = [ + 'v', 'o', 's', 'i', + 'u', 'e', 'p', 'c', + 'b', 't', 'r', 'z', 'a' +]; +var defaultInnerOrder = ['i', 'c', 'b', 'a']; + + +var writer$1 = function (session, opts) { + opts = opts || {}; + // ensure certain properties exist + if (session.version == null) { + session.version = 0; // 'v=0' must be there (only defined version atm) + } + if (session.name == null) { + session.name = ' '; // 's= ' must be there if no meaningful name set + } + session.media.forEach(function (mLine) { + if (mLine.payloads == null) { + mLine.payloads = ''; + } + }); + + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; + + // loop through outerOrder for matching properties on session + outerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in session && session[obj.name] != null) { + sdp.push(makeLine(type, obj, session)); + } + else if (obj.push in session && session[obj.push] != null) { + session[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); + + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); + } + else if (obj.push in mLine && mLine[obj.push] != null) { + mLine[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + }); + + return sdp.join('\r\n') + '\r\n'; +}; + +var parser = parser$1; +var writer = writer$1; + +lib.write = writer; +lib.parse = parser.parse; +lib.parseParams = parser.parseParams; +lib.parseFmtpConfig = parser.parseFmtpConfig; // Alias of parseParams(). +lib.parsePayloads = parser.parsePayloads; +lib.parseRemoteCandidates = parser.parseRemoteCandidates; +lib.parseImageAttributes = parser.parseImageAttributes; +lib.parseSimulcastStreamList = parser.parseSimulcastStreamList; + +var commonUtils = {}; + +var __createBinding$f = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$f = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$f = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$f(result, mod, k); + __setModuleDefault$f(result, mod); + return result; +}; +Object.defineProperty(commonUtils, "__esModule", { value: true }); +commonUtils.applyCodecParameters = commonUtils.getCname = commonUtils.extractDtlsParameters = commonUtils.extractRtpCapabilities = void 0; +const sdpTransform$c = __importStar$f(lib); +/** + * This function must be called with an SDP with 1 m=audio and 1 m=video + * sections. + */ +function extractRtpCapabilities({ sdpObject }) { + // Map of RtpCodecParameters indexed by payload type. + const codecsMap = new Map(); + // Array of RtpHeaderExtensions. + const headerExtensions = []; + // Whether a m=audio/video section has been already found. + let gotAudio = false; + let gotVideo = false; + for (const m of sdpObject.media) { + const kind = m.type; + switch (kind) { + case 'audio': + { + if (gotAudio) { + continue; + } + gotAudio = true; + break; + } + case 'video': + { + if (gotVideo) { + continue; + } + gotVideo = true; + break; + } + default: + { + continue; + } + } + // Get codecs. + for (const rtp of m.rtp) { + const codec = { + kind: kind, + mimeType: `${kind}/${rtp.codec}`, + preferredPayloadType: rtp.payload, + clockRate: rtp.rate, + channels: rtp.encoding, + parameters: {}, + rtcpFeedback: [] + }; + codecsMap.set(codec.preferredPayloadType, codec); + } + // Get codec parameters. + for (const fmtp of m.fmtp || []) { + const parameters = sdpTransform$c.parseParams(fmtp.config); + const codec = codecsMap.get(fmtp.payload); + if (!codec) { + continue; + } + // Specials case to convert parameter value to string. + if (parameters && parameters.hasOwnProperty('profile-level-id')) { + parameters['profile-level-id'] = String(parameters['profile-level-id']); + } + codec.parameters = parameters; + } + // Get RTCP feedback for each codec. + for (const fb of m.rtcpFb || []) { + const feedback = { + type: fb.type, + parameter: fb.subtype + }; + if (!feedback.parameter) { + delete feedback.parameter; + } + // rtcp-fb payload is not '*', so just apply it to its corresponding + // codec. + if (fb.payload !== '*') { + const codec = codecsMap.get(fb.payload); + if (!codec) { + continue; + } + codec.rtcpFeedback.push(feedback); + } + // If rtcp-fb payload is '*' it must be applied to all codecs with same + // kind (with some exceptions such as RTX codec). + else { + for (const codec of codecsMap.values()) { + if (codec.kind === kind && !/.+\/rtx$/i.test(codec.mimeType)) { + codec.rtcpFeedback.push(feedback); + } + } + } + } + // Get RTP header extensions. + for (const ext of m.ext || []) { + // Ignore encrypted extensions (not yet supported in mediasoup). + if (ext['encrypt-uri']) { + continue; + } + const headerExtension = { + kind: kind, + uri: ext.uri, + preferredId: ext.value + }; + headerExtensions.push(headerExtension); + } + } + const rtpCapabilities = { + codecs: Array.from(codecsMap.values()), + headerExtensions: headerExtensions + }; + return rtpCapabilities; +} +commonUtils.extractRtpCapabilities = extractRtpCapabilities; +function extractDtlsParameters({ sdpObject }) { + let setup = sdpObject.setup; + let fingerprint = sdpObject.fingerprint; + if (!setup || !fingerprint) { + const mediaObject = (sdpObject.media || []) + .find((m) => (m.port !== 0)); + if (mediaObject) { + setup ?? (setup = mediaObject.setup); + fingerprint ?? (fingerprint = mediaObject.fingerprint); + } + } + if (!setup) { + throw new Error('no a=setup found at SDP session or media level'); + } + else if (!fingerprint) { + throw new Error('no a=fingerprint found at SDP session or media level'); + } + let role; + switch (setup) { + case 'active': + role = 'client'; + break; + case 'passive': + role = 'server'; + break; + case 'actpass': + role = 'auto'; + break; + } + const dtlsParameters = { + role, + fingerprints: [ + { + algorithm: fingerprint.type, + value: fingerprint.hash + } + ] + }; + return dtlsParameters; +} +commonUtils.extractDtlsParameters = extractDtlsParameters; +function getCname({ offerMediaObject }) { + const ssrcCnameLine = (offerMediaObject.ssrcs || []) + .find((line) => line.attribute === 'cname'); + if (!ssrcCnameLine) { + return ''; + } + return ssrcCnameLine.value; +} +commonUtils.getCname = getCname; +/** + * Apply codec parameters in the given SDP m= section answer based on the + * given RTP parameters of an offer. + */ +function applyCodecParameters({ offerRtpParameters, answerMediaObject }) { + for (const codec of offerRtpParameters.codecs) { + const mimeType = codec.mimeType.toLowerCase(); + // Avoid parsing codec parameters for unhandled codecs. + if (mimeType !== 'audio/opus') { + continue; + } + const rtp = (answerMediaObject.rtp || []) + .find((r) => r.payload === codec.payloadType); + if (!rtp) { + continue; + } + // Just in case. + answerMediaObject.fmtp = answerMediaObject.fmtp || []; + let fmtp = answerMediaObject.fmtp + .find((f) => f.payload === codec.payloadType); + if (!fmtp) { + fmtp = { payload: codec.payloadType, config: '' }; + answerMediaObject.fmtp.push(fmtp); + } + const parameters = sdpTransform$c.parseParams(fmtp.config); + switch (mimeType) { + case 'audio/opus': + { + const spropStereo = codec.parameters['sprop-stereo']; + if (spropStereo !== undefined) { + parameters.stereo = spropStereo ? 1 : 0; + } + break; + } + } + // Write the codec fmtp.config back. + fmtp.config = ''; + for (const key of Object.keys(parameters)) { + if (fmtp.config) { + fmtp.config += ';'; + } + fmtp.config += `${key}=${parameters[key]}`; + } + } +} +commonUtils.applyCodecParameters = applyCodecParameters; + +var unifiedPlanUtils = {}; + +Object.defineProperty(unifiedPlanUtils, "__esModule", { value: true }); +unifiedPlanUtils.addLegacySimulcast = unifiedPlanUtils.getRtpEncodings = void 0; +function getRtpEncodings$1({ offerMediaObject }) { + const ssrcs = new Set(); + for (const line of offerMediaObject.ssrcs || []) { + const ssrc = line.id; + ssrcs.add(ssrc); + } + if (ssrcs.size === 0) { + throw new Error('no a=ssrc lines found'); + } + const ssrcToRtxSsrc = new Map(); + // First assume RTX is used. + for (const line of offerMediaObject.ssrcGroups || []) { + if (line.semantics !== 'FID') { + continue; + } + let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); + ssrc = Number(ssrc); + rtxSsrc = Number(rtxSsrc); + if (ssrcs.has(ssrc)) { + // Remove both the SSRC and RTX SSRC from the set so later we know + // that they are already handled. + ssrcs.delete(ssrc); + ssrcs.delete(rtxSsrc); + // Add to the map. + ssrcToRtxSsrc.set(ssrc, rtxSsrc); + } + } + // If the set of SSRCs is not empty it means that RTX is not being used, so + // take media SSRCs from there. + for (const ssrc of ssrcs) { + // Add to the map. + ssrcToRtxSsrc.set(ssrc, null); + } + const encodings = []; + for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { + const encoding = { ssrc }; + if (rtxSsrc) { + encoding.rtx = { ssrc: rtxSsrc }; + } + encodings.push(encoding); + } + return encodings; +} +unifiedPlanUtils.getRtpEncodings = getRtpEncodings$1; +/** + * Adds multi-ssrc based simulcast into the given SDP media section offer. + */ +function addLegacySimulcast$1({ offerMediaObject, numStreams }) { + if (numStreams <= 1) { + throw new TypeError('numStreams must be greater than 1'); + } + // Get the SSRC. + const ssrcMsidLine = (offerMediaObject.ssrcs || []) + .find((line) => line.attribute === 'msid'); + if (!ssrcMsidLine) { + throw new Error('a=ssrc line with msid information not found'); + } + const [streamId, trackId] = ssrcMsidLine.value.split(' '); + const firstSsrc = ssrcMsidLine.id; + let firstRtxSsrc; + // Get the SSRC for RTX. + (offerMediaObject.ssrcGroups || []) + .some((line) => { + if (line.semantics !== 'FID') { + return false; + } + const ssrcs = line.ssrcs.split(/\s+/); + if (Number(ssrcs[0]) === firstSsrc) { + firstRtxSsrc = Number(ssrcs[1]); + return true; + } + else { + return false; + } + }); + const ssrcCnameLine = offerMediaObject.ssrcs + .find((line) => line.attribute === 'cname'); + if (!ssrcCnameLine) { + throw new Error('a=ssrc line with cname information not found'); + } + const cname = ssrcCnameLine.value; + const ssrcs = []; + const rtxSsrcs = []; + for (let i = 0; i < numStreams; ++i) { + ssrcs.push(firstSsrc + i); + if (firstRtxSsrc) { + rtxSsrcs.push(firstRtxSsrc + i); + } + } + offerMediaObject.ssrcGroups = []; + offerMediaObject.ssrcs = []; + offerMediaObject.ssrcGroups.push({ + semantics: 'SIM', + ssrcs: ssrcs.join(' ') + }); + for (let i = 0; i < ssrcs.length; ++i) { + const ssrc = ssrcs[i]; + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId} ${trackId}` + }); + } + for (let i = 0; i < rtxSsrcs.length; ++i) { + const ssrc = ssrcs[i]; + const rtxSsrc = rtxSsrcs[i]; + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId} ${trackId}` + }); + offerMediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}` + }); + } +} +unifiedPlanUtils.addLegacySimulcast = addLegacySimulcast$1; + +var utils$e = {}; + +Object.defineProperty(utils$e, "__esModule", { value: true }); +utils$e.addNackSuppportForOpus = void 0; +/** + * This function adds RTCP NACK support for OPUS codec in given capabilities. + */ +function addNackSuppportForOpus(rtpCapabilities) { + for (const codec of (rtpCapabilities.codecs || [])) { + if ((codec.mimeType.toLowerCase() === 'audio/opus' || + codec.mimeType.toLowerCase() === 'audio/multiopus') && + !codec.rtcpFeedback?.some((fb) => fb.type === 'nack' && !fb.parameter)) { + if (!codec.rtcpFeedback) { + codec.rtcpFeedback = []; + } + codec.rtcpFeedback.push({ type: 'nack' }); + } + } +} +utils$e.addNackSuppportForOpus = addNackSuppportForOpus; + +var HandlerInterface$1 = {}; + +Object.defineProperty(HandlerInterface$1, "__esModule", { value: true }); +HandlerInterface$1.HandlerInterface = void 0; +const EnhancedEventEmitter_1$1 = EnhancedEventEmitter$1; +class HandlerInterface extends EnhancedEventEmitter_1$1.EnhancedEventEmitter { + constructor() { + super(); + } +} +HandlerInterface$1.HandlerInterface = HandlerInterface; + +var RemoteSdp$1 = {}; + +var MediaSection$1 = {}; + +var __createBinding$e = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$e = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$e = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$e(result, mod, k); + __setModuleDefault$e(result, mod); + return result; +}; +Object.defineProperty(MediaSection$1, "__esModule", { value: true }); +MediaSection$1.OfferMediaSection = MediaSection$1.AnswerMediaSection = MediaSection$1.MediaSection = void 0; +const sdpTransform$b = __importStar$e(lib); +const utils$d = __importStar$e(utils$h); +class MediaSection { + constructor({ iceParameters, iceCandidates, dtlsParameters, planB = false }) { + this._mediaObject = {}; + this._planB = planB; + if (iceParameters) { + this.setIceParameters(iceParameters); + } + if (iceCandidates) { + this._mediaObject.candidates = []; + for (const candidate of iceCandidates) { + const candidateObject = {}; + // mediasoup does mandates rtcp-mux so candidates component is always + // RTP (1). + candidateObject.component = 1; + candidateObject.foundation = candidate.foundation; + candidateObject.ip = candidate.ip; + candidateObject.port = candidate.port; + candidateObject.priority = candidate.priority; + candidateObject.transport = candidate.protocol; + candidateObject.type = candidate.type; + if (candidate.tcpType) { + candidateObject.tcptype = candidate.tcpType; + } + this._mediaObject.candidates.push(candidateObject); + } + this._mediaObject.endOfCandidates = 'end-of-candidates'; + this._mediaObject.iceOptions = 'renomination'; + } + if (dtlsParameters) { + this.setDtlsRole(dtlsParameters.role); + } + } + get mid() { + return String(this._mediaObject.mid); + } + get closed() { + return this._mediaObject.port === 0; + } + getObject() { + return this._mediaObject; + } + setIceParameters(iceParameters) { + this._mediaObject.iceUfrag = iceParameters.usernameFragment; + this._mediaObject.icePwd = iceParameters.password; + } + pause() { + this._mediaObject.direction = 'inactive'; + } + disable() { + this.pause(); + delete this._mediaObject.ext; + delete this._mediaObject.ssrcs; + delete this._mediaObject.ssrcGroups; + delete this._mediaObject.simulcast; + delete this._mediaObject.simulcast_03; + delete this._mediaObject.rids; + delete this._mediaObject.extmapAllowMixed; + } + close() { + this.disable(); + this._mediaObject.port = 0; + } +} +MediaSection$1.MediaSection = MediaSection; +class AnswerMediaSection extends MediaSection { + constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, planB = false, offerMediaObject, offerRtpParameters, answerRtpParameters, codecOptions, extmapAllowMixed = false }) { + super({ iceParameters, iceCandidates, dtlsParameters, planB }); + this._mediaObject.mid = String(offerMediaObject.mid); + this._mediaObject.type = offerMediaObject.type; + this._mediaObject.protocol = offerMediaObject.protocol; + if (!plainRtpParameters) { + this._mediaObject.connection = { ip: '127.0.0.1', version: 4 }; + this._mediaObject.port = 7; + } + else { + this._mediaObject.connection = + { + ip: plainRtpParameters.ip, + version: plainRtpParameters.ipVersion + }; + this._mediaObject.port = plainRtpParameters.port; + } + switch (offerMediaObject.type) { + case 'audio': + case 'video': + { + this._mediaObject.direction = 'recvonly'; + this._mediaObject.rtp = []; + this._mediaObject.rtcpFb = []; + this._mediaObject.fmtp = []; + for (const codec of answerRtpParameters.codecs) { + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate + }; + if (codec.channels > 1) { + rtp.encoding = codec.channels; + } + this._mediaObject.rtp.push(rtp); + const codecParameters = utils$d.clone(codec.parameters) ?? {}; + let codecRtcpFeedback = utils$d.clone(codec.rtcpFeedback) ?? []; + if (codecOptions) { + const { opusStereo, opusFec, opusDtx, opusMaxPlaybackRate, opusMaxAverageBitrate, opusPtime, opusNack, videoGoogleStartBitrate, videoGoogleMaxBitrate, videoGoogleMinBitrate } = codecOptions; + const offerCodec = offerRtpParameters.codecs + .find((c) => (c.payloadType === codec.payloadType)); + switch (codec.mimeType.toLowerCase()) { + case 'audio/opus': + case 'audio/multiopus': + { + if (opusStereo !== undefined) { + offerCodec.parameters['sprop-stereo'] = opusStereo ? 1 : 0; + codecParameters.stereo = opusStereo ? 1 : 0; + } + if (opusFec !== undefined) { + offerCodec.parameters.useinbandfec = opusFec ? 1 : 0; + codecParameters.useinbandfec = opusFec ? 1 : 0; + } + if (opusDtx !== undefined) { + offerCodec.parameters.usedtx = opusDtx ? 1 : 0; + codecParameters.usedtx = opusDtx ? 1 : 0; + } + if (opusMaxPlaybackRate !== undefined) { + codecParameters.maxplaybackrate = opusMaxPlaybackRate; + } + if (opusMaxAverageBitrate !== undefined) { + codecParameters.maxaveragebitrate = opusMaxAverageBitrate; + } + if (opusPtime !== undefined) { + offerCodec.parameters.ptime = opusPtime; + codecParameters.ptime = opusPtime; + } + // If opusNack is not set, we must remove NACK support for OPUS. + // Otherwise it would be enabled for those handlers that artificially + // announce it in their RTP capabilities. + if (!opusNack) { + offerCodec.rtcpFeedback = offerCodec + .rtcpFeedback + .filter((fb) => fb.type !== 'nack' || fb.parameter); + codecRtcpFeedback = codecRtcpFeedback + .filter((fb) => fb.type !== 'nack' || fb.parameter); + } + break; + } + case 'video/vp8': + case 'video/vp9': + case 'video/h264': + case 'video/h265': + { + if (videoGoogleStartBitrate !== undefined) { + codecParameters['x-google-start-bitrate'] = videoGoogleStartBitrate; + } + if (videoGoogleMaxBitrate !== undefined) { + codecParameters['x-google-max-bitrate'] = videoGoogleMaxBitrate; + } + if (videoGoogleMinBitrate !== undefined) { + codecParameters['x-google-min-bitrate'] = videoGoogleMinBitrate; + } + break; + } + } + } + const fmtp = { + payload: codec.payloadType, + config: '' + }; + for (const key of Object.keys(codecParameters)) { + if (fmtp.config) { + fmtp.config += ';'; + } + fmtp.config += `${key}=${codecParameters[key]}`; + } + if (fmtp.config) { + this._mediaObject.fmtp.push(fmtp); + } + for (const fb of codecRtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter + }); + } + } + this._mediaObject.payloads = answerRtpParameters.codecs + .map((codec) => codec.payloadType) + .join(' '); + this._mediaObject.ext = []; + for (const ext of answerRtpParameters.headerExtensions) { + // Don't add a header extension if not present in the offer. + const found = (offerMediaObject.ext || []) + .some((localExt) => localExt.uri === ext.uri); + if (!found) { + continue; + } + this._mediaObject.ext.push({ + uri: ext.uri, + value: ext.id + }); + } + // Allow both 1 byte and 2 bytes length header extensions. + if (extmapAllowMixed && + offerMediaObject.extmapAllowMixed === 'extmap-allow-mixed') { + this._mediaObject.extmapAllowMixed = 'extmap-allow-mixed'; + } + // Simulcast. + if (offerMediaObject.simulcast) { + this._mediaObject.simulcast = + { + dir1: 'recv', + list1: offerMediaObject.simulcast.list1 + }; + this._mediaObject.rids = []; + for (const rid of offerMediaObject.rids || []) { + if (rid.direction !== 'send') { + continue; + } + this._mediaObject.rids.push({ + id: rid.id, + direction: 'recv' + }); + } + } + // Simulcast (draft version 03). + else if (offerMediaObject.simulcast_03) { + // eslint-disable-next-line camelcase + this._mediaObject.simulcast_03 = + { + value: offerMediaObject.simulcast_03.value.replace(/send/g, 'recv') + }; + this._mediaObject.rids = []; + for (const rid of offerMediaObject.rids || []) { + if (rid.direction !== 'send') { + continue; + } + this._mediaObject.rids.push({ + id: rid.id, + direction: 'recv' + }); + } + } + this._mediaObject.rtcpMux = 'rtcp-mux'; + this._mediaObject.rtcpRsize = 'rtcp-rsize'; + if (this._planB && this._mediaObject.type === 'video') { + this._mediaObject.xGoogleFlag = 'conference'; + } + break; + } + case 'application': + { + // New spec. + if (typeof offerMediaObject.sctpPort === 'number') { + this._mediaObject.payloads = 'webrtc-datachannel'; + this._mediaObject.sctpPort = sctpParameters.port; + this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize; + } + // Old spec. + else if (offerMediaObject.sctpmap) { + this._mediaObject.payloads = sctpParameters.port; + this._mediaObject.sctpmap = + { + app: 'webrtc-datachannel', + sctpmapNumber: sctpParameters.port, + maxMessageSize: sctpParameters.maxMessageSize + }; + } + break; + } + } + } + setDtlsRole(role) { + switch (role) { + case 'client': + this._mediaObject.setup = 'active'; + break; + case 'server': + this._mediaObject.setup = 'passive'; + break; + case 'auto': + this._mediaObject.setup = 'actpass'; + break; + } + } + resume() { + this._mediaObject.direction = 'recvonly'; + } + muxSimulcastStreams(encodings) { + if (!this._mediaObject.simulcast || !this._mediaObject.simulcast.list1) { + return; + } + const layers = {}; + for (const encoding of encodings) { + if (encoding.rid) { + layers[encoding.rid] = encoding; + } + } + const raw = this._mediaObject.simulcast.list1; + const simulcastStreams = sdpTransform$b.parseSimulcastStreamList(raw); + for (const simulcastStream of simulcastStreams) { + for (const simulcastFormat of simulcastStream) { + simulcastFormat.paused = !layers[simulcastFormat.scid]?.active; + } + } + this._mediaObject.simulcast.list1 = simulcastStreams.map((simulcastFormats) => simulcastFormats.map((f) => `${f.paused ? '~' : ''}${f.scid}`).join(',')).join(';'); + } +} +MediaSection$1.AnswerMediaSection = AnswerMediaSection; +class OfferMediaSection extends MediaSection { + constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, planB = false, mid, kind, offerRtpParameters, streamId, trackId, oldDataChannelSpec = false }) { + super({ iceParameters, iceCandidates, dtlsParameters, planB }); + this._mediaObject.mid = String(mid); + this._mediaObject.type = kind; + if (!plainRtpParameters) { + this._mediaObject.connection = { ip: '127.0.0.1', version: 4 }; + if (!sctpParameters) { + this._mediaObject.protocol = 'UDP/TLS/RTP/SAVPF'; + } + else { + this._mediaObject.protocol = 'UDP/DTLS/SCTP'; + } + this._mediaObject.port = 7; + } + else { + this._mediaObject.connection = + { + ip: plainRtpParameters.ip, + version: plainRtpParameters.ipVersion + }; + this._mediaObject.protocol = 'RTP/AVP'; + this._mediaObject.port = plainRtpParameters.port; + } + switch (kind) { + case 'audio': + case 'video': + { + this._mediaObject.direction = 'sendonly'; + this._mediaObject.rtp = []; + this._mediaObject.rtcpFb = []; + this._mediaObject.fmtp = []; + if (!this._planB) { + this._mediaObject.msid = `${streamId || '-'} ${trackId}`; + } + for (const codec of offerRtpParameters.codecs) { + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate + }; + if (codec.channels > 1) { + rtp.encoding = codec.channels; + } + this._mediaObject.rtp.push(rtp); + const fmtp = { + payload: codec.payloadType, + config: '' + }; + for (const key of Object.keys(codec.parameters)) { + if (fmtp.config) { + fmtp.config += ';'; + } + fmtp.config += `${key}=${codec.parameters[key]}`; + } + if (fmtp.config) { + this._mediaObject.fmtp.push(fmtp); + } + for (const fb of codec.rtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter + }); + } + } + this._mediaObject.payloads = offerRtpParameters.codecs + .map((codec) => codec.payloadType) + .join(' '); + this._mediaObject.ext = []; + for (const ext of offerRtpParameters.headerExtensions) { + this._mediaObject.ext.push({ + uri: ext.uri, + value: ext.id + }); + } + this._mediaObject.rtcpMux = 'rtcp-mux'; + this._mediaObject.rtcpRsize = 'rtcp-rsize'; + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = (encoding.rtx && encoding.rtx.ssrc) + ? encoding.rtx.ssrc + : undefined; + this._mediaObject.ssrcs = []; + this._mediaObject.ssrcGroups = []; + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname + }); + } + if (this._planB) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}` + }); + } + if (rtxSsrc) { + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname + }); + } + if (this._planB) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}` + }); + } + // Associate original and retransmission SSRCs. + this._mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}` + }); + } + break; + } + case 'application': + { + // New spec. + if (!oldDataChannelSpec) { + this._mediaObject.payloads = 'webrtc-datachannel'; + this._mediaObject.sctpPort = sctpParameters.port; + this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize; + } + // Old spec. + else { + this._mediaObject.payloads = sctpParameters.port; + this._mediaObject.sctpmap = + { + app: 'webrtc-datachannel', + sctpmapNumber: sctpParameters.port, + maxMessageSize: sctpParameters.maxMessageSize + }; + } + break; + } + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setDtlsRole(role) { + // Always 'actpass'. + this._mediaObject.setup = 'actpass'; + } + resume() { + this._mediaObject.direction = 'sendonly'; + } + planBReceive({ offerRtpParameters, streamId, trackId }) { + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = (encoding.rtx && encoding.rtx.ssrc) + ? encoding.rtx.ssrc + : undefined; + const payloads = this._mediaObject.payloads.split(' '); + for (const codec of offerRtpParameters.codecs) { + if (payloads.includes(String(codec.payloadType))) { + continue; + } + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate + }; + if (codec.channels > 1) { + rtp.encoding = codec.channels; + } + this._mediaObject.rtp.push(rtp); + const fmtp = { + payload: codec.payloadType, + config: '' + }; + for (const key of Object.keys(codec.parameters)) { + if (fmtp.config) { + fmtp.config += ';'; + } + fmtp.config += `${key}=${codec.parameters[key]}`; + } + if (fmtp.config) { + this._mediaObject.fmtp.push(fmtp); + } + for (const fb of codec.rtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter + }); + } + } + this._mediaObject.payloads += ` ${offerRtpParameters + .codecs + .filter((codec) => !this._mediaObject.payloads.includes(codec.payloadType)) + .map((codec) => codec.payloadType) + .join(' ')}`; + this._mediaObject.payloads = this._mediaObject.payloads.trim(); + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname + }); + } + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}` + }); + if (rtxSsrc) { + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname + }); + } + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}` + }); + // Associate original and retransmission SSRCs. + this._mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}` + }); + } + } + planBStopReceiving({ offerRtpParameters }) { + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = (encoding.rtx && encoding.rtx.ssrc) + ? encoding.rtx.ssrc + : undefined; + this._mediaObject.ssrcs = this._mediaObject.ssrcs + .filter((s) => s.id !== ssrc && s.id !== rtxSsrc); + if (rtxSsrc) { + this._mediaObject.ssrcGroups = this._mediaObject.ssrcGroups + .filter((group) => group.ssrcs !== `${ssrc} ${rtxSsrc}`); + } + } +} +MediaSection$1.OfferMediaSection = OfferMediaSection; +function getCodecName(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) { + throw new TypeError('invalid codec.mimeType'); + } + return mimeTypeMatch[2]; +} + +var __createBinding$d = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$d = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$d = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$d(result, mod, k); + __setModuleDefault$d(result, mod); + return result; +}; +Object.defineProperty(RemoteSdp$1, "__esModule", { value: true }); +RemoteSdp$1.RemoteSdp = void 0; +const sdpTransform$a = __importStar$d(lib); +const Logger_1$c = Logger$3; +const MediaSection_1 = MediaSection$1; +const logger$c = new Logger_1$c.Logger('RemoteSdp'); +class RemoteSdp { + constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, planB = false }) { + // MediaSection instances with same order as in the SDP. + this._mediaSections = []; + // MediaSection indices indexed by MID. + this._midToIndex = new Map(); + this._iceParameters = iceParameters; + this._iceCandidates = iceCandidates; + this._dtlsParameters = dtlsParameters; + this._sctpParameters = sctpParameters; + this._plainRtpParameters = plainRtpParameters; + this._planB = planB; + this._sdpObject = + { + version: 0, + origin: { + address: '0.0.0.0', + ipVer: 4, + netType: 'IN', + sessionId: 10000, + sessionVersion: 0, + username: 'mediasoup-client' + }, + name: '-', + timing: { start: 0, stop: 0 }, + media: [] + }; + // If ICE parameters are given, add ICE-Lite indicator. + if (iceParameters && iceParameters.iceLite) { + this._sdpObject.icelite = 'ice-lite'; + } + // If DTLS parameters are given, assume WebRTC and BUNDLE. + if (dtlsParameters) { + this._sdpObject.msidSemantic = { semantic: 'WMS', token: '*' }; + // NOTE: We take the latest fingerprint. + const numFingerprints = this._dtlsParameters.fingerprints.length; + this._sdpObject.fingerprint = + { + type: dtlsParameters.fingerprints[numFingerprints - 1].algorithm, + hash: dtlsParameters.fingerprints[numFingerprints - 1].value + }; + this._sdpObject.groups = [{ type: 'BUNDLE', mids: '' }]; + } + // If there are plain RPT parameters, override SDP origin. + if (plainRtpParameters) { + this._sdpObject.origin.address = plainRtpParameters.ip; + this._sdpObject.origin.ipVer = plainRtpParameters.ipVersion; + } + } + updateIceParameters(iceParameters) { + logger$c.debug('updateIceParameters() [iceParameters:%o]', iceParameters); + this._iceParameters = iceParameters; + this._sdpObject.icelite = iceParameters.iceLite ? 'ice-lite' : undefined; + for (const mediaSection of this._mediaSections) { + mediaSection.setIceParameters(iceParameters); + } + } + updateDtlsRole(role) { + logger$c.debug('updateDtlsRole() [role:%s]', role); + this._dtlsParameters.role = role; + for (const mediaSection of this._mediaSections) { + mediaSection.setDtlsRole(role); + } + } + getNextMediaSectionIdx() { + // If a closed media section is found, return its index. + for (let idx = 0; idx < this._mediaSections.length; ++idx) { + const mediaSection = this._mediaSections[idx]; + if (mediaSection.closed) { + return { idx, reuseMid: mediaSection.mid }; + } + } + // If no closed media section is found, return next one. + return { idx: this._mediaSections.length }; + } + send({ offerMediaObject, reuseMid, offerRtpParameters, answerRtpParameters, codecOptions, extmapAllowMixed = false }) { + const mediaSection = new MediaSection_1.AnswerMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + plainRtpParameters: this._plainRtpParameters, + planB: this._planB, + offerMediaObject, + offerRtpParameters, + answerRtpParameters, + codecOptions, + extmapAllowMixed + }); + // Unified-Plan with closed media section replacement. + if (reuseMid) { + this._replaceMediaSection(mediaSection, reuseMid); + } + // Unified-Plan or Plan-B with different media kind. + else if (!this._midToIndex.has(mediaSection.mid)) { + this._addMediaSection(mediaSection); + } + // Plan-B with same media kind. + else { + this._replaceMediaSection(mediaSection); + } + } + receive({ mid, kind, offerRtpParameters, streamId, trackId }) { + const idx = this._midToIndex.get(mid); + let mediaSection; + if (idx !== undefined) { + mediaSection = this._mediaSections[idx]; + } + // Unified-Plan or different media kind. + if (!mediaSection) { + mediaSection = new MediaSection_1.OfferMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + plainRtpParameters: this._plainRtpParameters, + planB: this._planB, + mid, + kind, + offerRtpParameters, + streamId, + trackId + }); + // Let's try to recycle a closed media section (if any). + // NOTE: Yes, we can recycle a closed m=audio section with a new m=video. + const oldMediaSection = this._mediaSections.find((m) => (m.closed)); + if (oldMediaSection) { + this._replaceMediaSection(mediaSection, oldMediaSection.mid); + } + else { + this._addMediaSection(mediaSection); + } + } + // Plan-B. + else { + mediaSection.planBReceive({ offerRtpParameters, streamId, trackId }); + this._replaceMediaSection(mediaSection); + } + } + pauseMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.pause(); + } + resumeSendingMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.resume(); + } + resumeReceivingMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.resume(); + } + disableMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.disable(); + } + /** + * Closes media section. Returns true if the given MID corresponds to a m + * section that has been indeed closed. False otherwise. + * + * NOTE: Closing the first m section is a pain since it invalidates the bundled + * transport, so instead closing it we just disable it. + */ + closeMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + // NOTE: Closing the first m section is a pain since it invalidates the + // bundled transport, so let's avoid it. + if (mid === this._firstMid) { + logger$c.debug('closeMediaSection() | cannot close first media section, disabling it instead [mid:%s]', mid); + this.disableMediaSection(mid); + return false; + } + mediaSection.close(); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + return true; + } + muxMediaSectionSimulcast(mid, encodings) { + const mediaSection = this._findMediaSection(mid); + mediaSection.muxSimulcastStreams(encodings); + this._replaceMediaSection(mediaSection); + } + planBStopReceiving({ mid, offerRtpParameters }) { + const mediaSection = this._findMediaSection(mid); + mediaSection.planBStopReceiving({ offerRtpParameters }); + this._replaceMediaSection(mediaSection); + } + sendSctpAssociation({ offerMediaObject }) { + const mediaSection = new MediaSection_1.AnswerMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + sctpParameters: this._sctpParameters, + plainRtpParameters: this._plainRtpParameters, + offerMediaObject + }); + this._addMediaSection(mediaSection); + } + receiveSctpAssociation({ oldDataChannelSpec = false } = {}) { + const mediaSection = new MediaSection_1.OfferMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + sctpParameters: this._sctpParameters, + plainRtpParameters: this._plainRtpParameters, + mid: 'datachannel', + kind: 'application', + oldDataChannelSpec + }); + this._addMediaSection(mediaSection); + } + getSdp() { + // Increase SDP version. + this._sdpObject.origin.sessionVersion++; + return sdpTransform$a.write(this._sdpObject); + } + _addMediaSection(newMediaSection) { + if (!this._firstMid) { + this._firstMid = newMediaSection.mid; + } + // Add to the vector. + this._mediaSections.push(newMediaSection); + // Add to the map. + this._midToIndex.set(newMediaSection.mid, this._mediaSections.length - 1); + // Add to the SDP object. + this._sdpObject.media.push(newMediaSection.getObject()); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + } + _replaceMediaSection(newMediaSection, reuseMid) { + // Store it in the map. + if (typeof reuseMid === 'string') { + const idx = this._midToIndex.get(reuseMid); + if (idx === undefined) { + throw new Error(`no media section found for reuseMid '${reuseMid}'`); + } + const oldMediaSection = this._mediaSections[idx]; + // Replace the index in the vector with the new media section. + this._mediaSections[idx] = newMediaSection; + // Update the map. + this._midToIndex.delete(oldMediaSection.mid); + this._midToIndex.set(newMediaSection.mid, idx); + // Update the SDP object. + this._sdpObject.media[idx] = newMediaSection.getObject(); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + } + else { + const idx = this._midToIndex.get(newMediaSection.mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${newMediaSection.mid}'`); + } + // Replace the index in the vector with the new media section. + this._mediaSections[idx] = newMediaSection; + // Update the SDP object. + this._sdpObject.media[idx] = newMediaSection.getObject(); + } + } + _findMediaSection(mid) { + const idx = this._midToIndex.get(mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${mid}'`); + } + return this._mediaSections[idx]; + } + _regenerateBundleMids() { + if (!this._dtlsParameters) { + return; + } + this._sdpObject.groups[0].mids = this._mediaSections + .filter((mediaSection) => !mediaSection.closed) + .map((mediaSection) => mediaSection.mid) + .join(' '); + } +} +RemoteSdp$1.RemoteSdp = RemoteSdp; + +var scalabilityModes = {}; + +Object.defineProperty(scalabilityModes, "__esModule", { value: true }); +scalabilityModes.parse = void 0; +const ScalabilityModeRegex = new RegExp('^[LS]([1-9]\\d{0,1})T([1-9]\\d{0,1})'); +function parse(scalabilityMode) { + const match = ScalabilityModeRegex.exec(scalabilityMode || ''); + if (match) { + return { + spatialLayers: Number(match[1]), + temporalLayers: Number(match[2]) + }; + } + else { + return { + spatialLayers: 1, + temporalLayers: 1 + }; + } +} +scalabilityModes.parse = parse; + +var __createBinding$c = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$c = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$c = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$c(result, mod, k); + __setModuleDefault$c(result, mod); + return result; +}; +Object.defineProperty(Chrome111$1, "__esModule", { value: true }); +Chrome111$1.Chrome111 = void 0; +const sdpTransform$9 = __importStar$c(lib); +const Logger_1$b = Logger$3; +const utils$c = __importStar$c(utils$h); +const ortc$b = __importStar$c(ortc$d); +const sdpCommonUtils$9 = __importStar$c(commonUtils); +const sdpUnifiedPlanUtils$5 = __importStar$c(unifiedPlanUtils); +const ortcUtils$3 = __importStar$c(utils$e); +const errors_1$8 = errors; +const HandlerInterface_1$a = HandlerInterface$1; +const RemoteSdp_1$9 = RemoteSdp$1; +const scalabilityModes_1$5 = scalabilityModes; +const logger$b = new Logger_1$b.Logger('Chrome111'); +const SCTP_NUM_STREAMS$9 = { OS: 1024, MIS: 1024 }; +class Chrome111 extends HandlerInterface_1$a.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome111(); + } + constructor() { + super(); + // Closed flag. + this._closed = false; + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Chrome111'; + } + close() { + logger$b.debug('close()'); + if (this._closed) { + return; + } + this._closed = true; + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$b.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan' + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$9.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$9.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils$3.addNackSuppportForOpus(nativeRtpCapabilities); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$b.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$9 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + this.assertNotClosed(); + logger$b.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$9.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$b.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$b.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$b.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$b.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + logger$b.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + this.assertNotClosed(); + logger$b.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + this.assertNotClosed(); + logger$b.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$b.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$b.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + this.assertNotClosed(); + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$b.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + // Set rid and verify scalabilityMode in each encoding. + // NOTE: Even if WebRTC allows different scalabilityMode (different number + // of temporal layers) per simulcast stream, we need that those are the + // same in all them, so let's pick up the highest value. + // NOTE: If scalabilityMode is not given, Chrome will use L1T3. + let nextRid = 1; + let maxTemporalLayers = 1; + for (const encoding of encodings) { + const temporalLayers = encoding.scalabilityMode + ? (0, scalabilityModes_1$5.parse)(encoding.scalabilityMode).temporalLayers + : 3; + if (temporalLayers > maxTemporalLayers) { + maxTemporalLayers = temporalLayers; + } + } + for (const encoding of encodings) { + encoding.rid = `r${nextRid++}`; + encoding.scalabilityMode = `L1T${maxTemporalLayers}`; + } + } + const sendingRtpParameters = utils$c.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$b.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$c.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$b.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$9.parse(offer.sdp); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$b.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$9.parse(this._pc.localDescription.sdp); + const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$9.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils$5.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + const newEncodings = sdpUnifiedPlanUtils$5.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$b.debug('stopSending() [localId:%s]', localId); + if (this._closed) { + return; + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger$b.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + async pauseSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$b.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$b.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async resumeSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$b.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger$b.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertNotClosed(); + this.assertSendDirection(); + if (track) { + logger$b.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$b.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$b.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$b.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$b.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$b.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertNotClosed(); + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$b.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$9.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$9.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$b.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertNotClosed(); + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$b.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$9.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$9.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$9.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$b.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + if (this._closed) { + return; + } + for (const localId of localIds) { + logger$b.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$b.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$b.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$b.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$b.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$b.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertNotClosed(); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertNotClosed(); + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$b.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$b.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$9.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$b.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$9.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$9.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertNotClosed() { + if (this._closed) { + throw new errors_1$8.InvalidStateError('method called in a closed handler'); + } + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Chrome111$1.Chrome111 = Chrome111; + +var Chrome74$1 = {}; + +var __createBinding$b = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$b = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$b = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$b(result, mod, k); + __setModuleDefault$b(result, mod); + return result; +}; +Object.defineProperty(Chrome74$1, "__esModule", { value: true }); +Chrome74$1.Chrome74 = void 0; +const sdpTransform$8 = __importStar$b(lib); +const Logger_1$a = Logger$3; +const utils$b = __importStar$b(utils$h); +const ortc$a = __importStar$b(ortc$d); +const sdpCommonUtils$8 = __importStar$b(commonUtils); +const sdpUnifiedPlanUtils$4 = __importStar$b(unifiedPlanUtils); +const ortcUtils$2 = __importStar$b(utils$e); +const errors_1$7 = errors; +const HandlerInterface_1$9 = HandlerInterface$1; +const RemoteSdp_1$8 = RemoteSdp$1; +const scalabilityModes_1$4 = scalabilityModes; +const logger$a = new Logger_1$a.Logger('Chrome74'); +const SCTP_NUM_STREAMS$8 = { OS: 1024, MIS: 1024 }; +class Chrome74 extends HandlerInterface_1$9.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome74(); + } + constructor() { + super(); + // Closed flag. + this._closed = false; + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Chrome74'; + } + close() { + logger$a.debug('close()'); + if (this._closed) { + return; + } + this._closed = true; + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$a.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan' + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$8.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$8.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils$2.addNackSuppportForOpus(nativeRtpCapabilities); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$a.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$8 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$a.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$8.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$a.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$a.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$a.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$a.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + logger$a.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + this.assertNotClosed(); + logger$a.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + this.assertNotClosed(); + logger$a.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$a.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$a.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + this.assertNotClosed(); + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$a.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + } + const sendingRtpParameters = utils$b.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$a.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$b.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$a.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$8.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = (0, scalabilityModes_1$4.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { + logger$a.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; + localSdpObject = sdpTransform$8.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils$4.addLegacySimulcast({ + offerMediaObject, + numStreams: layers.spatialLayers + }); + offer = { type: 'offer', sdp: sdpTransform$8.write(localSdpObject) }; + } + logger$a.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$8.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$8.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils$4.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + let newEncodings = sdpUnifiedPlanUtils$4.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + // Hack for VP9 SVC. + if (hackVp9Svc) { + newEncodings = [newEncodings[0]]; + } + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$a.debug('stopSending() [localId:%s]', localId); + if (this._closed) { + return; + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger$a.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + async pauseSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$a.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$a.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async resumeSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$a.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger$a.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertNotClosed(); + this.assertSendDirection(); + if (track) { + logger$a.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$a.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$a.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$a.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$a.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$a.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertNotClosed(); + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$a.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$8.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$8.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$a.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertNotClosed(); + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$a.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$8.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$8.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$8.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$a.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + if (this._closed) { + return; + } + for (const localId of localIds) { + logger$a.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$a.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$a.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$a.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$a.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$a.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertNotClosed(); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertNotClosed(); + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$a.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$a.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$8.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$a.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$8.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$8.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertNotClosed() { + if (this._closed) { + throw new errors_1$7.InvalidStateError('method called in a closed handler'); + } + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Chrome74$1.Chrome74 = Chrome74; + +var Chrome70$1 = {}; + +var __createBinding$a = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$a = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$a = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$a(result, mod, k); + __setModuleDefault$a(result, mod); + return result; +}; +Object.defineProperty(Chrome70$1, "__esModule", { value: true }); +Chrome70$1.Chrome70 = void 0; +const sdpTransform$7 = __importStar$a(lib); +const Logger_1$9 = Logger$3; +const utils$a = __importStar$a(utils$h); +const ortc$9 = __importStar$a(ortc$d); +const sdpCommonUtils$7 = __importStar$a(commonUtils); +const sdpUnifiedPlanUtils$3 = __importStar$a(unifiedPlanUtils); +const HandlerInterface_1$8 = HandlerInterface$1; +const RemoteSdp_1$7 = RemoteSdp$1; +const scalabilityModes_1$3 = scalabilityModes; +const logger$9 = new Logger_1$9.Logger('Chrome70'); +const SCTP_NUM_STREAMS$7 = { OS: 1024, MIS: 1024 }; +class Chrome70 extends HandlerInterface_1$8.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome70(); + } + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Chrome70'; + } + close() { + logger$9.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$9.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan' + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$7.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$7.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$9.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$7 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$9.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$7.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$9.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$9.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$9.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$9.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$9.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + logger$9.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger$9.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$9.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$9.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); + logger$9.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + const sendingRtpParameters = utils$a.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$9.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$a.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$9.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$7.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + if (encodings && encodings.length > 1) { + logger$9.debug('send() | enabling legacy simulcast'); + localSdpObject = sdpTransform$7.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils$3.addLegacySimulcast({ + offerMediaObject, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform$7.write(localSdpObject) }; + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = (0, scalabilityModes_1$3.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { + logger$9.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; + localSdpObject = sdpTransform$7.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils$3.addLegacySimulcast({ + offerMediaObject, + numStreams: layers.spatialLayers + }); + offer = { type: 'offer', sdp: sdpTransform$7.write(localSdpObject) }; + } + logger$9.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // If encodings are given, apply them now. + if (encodings) { + logger$9.debug('send() | applying given encodings'); + const parameters = transceiver.sender.getParameters(); + for (let idx = 0; idx < (parameters.encodings || []).length; ++idx) { + const encoding = parameters.encodings[idx]; + const desiredEncoding = encodings[idx]; + // Should not happen but just in case. + if (!desiredEncoding) { + break; + } + parameters.encodings[idx] = Object.assign(encoding, desiredEncoding); + } + await transceiver.sender.setParameters(parameters); + } + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$7.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$7.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils$3.getRtpEncodings({ offerMediaObject }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // Hack for VP9 SVC. + if (hackVp9Svc) { + sendingRtpParameters.encodings = [sendingRtpParameters.encodings[0]]; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$9.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger$9.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger$9.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$9.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertSendDirection(); + logger$9.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$9.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertSendDirection(); + logger$9.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$9.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$9.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$7.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$7.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$9.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$9.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$7.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$7.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$7.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$9.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger$9.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$9.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async getReceiverStats(localId) { + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$9.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$9.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$7.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$9.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$7.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$7.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Chrome70$1.Chrome70 = Chrome70; + +var Chrome67$1 = {}; + +var planBUtils = {}; + +Object.defineProperty(planBUtils, "__esModule", { value: true }); +planBUtils.addLegacySimulcast = planBUtils.getRtpEncodings = void 0; +function getRtpEncodings({ offerMediaObject, track }) { + const ssrcs = new Set(); + for (const line of offerMediaObject.ssrcs || []) { + if (line.attribute !== 'msid') { + continue; + } + const trackId = line.value.split(' ')[1]; + if (trackId === track.id) { + const ssrc = line.id; + ssrcs.add(ssrc); + } + } + if (ssrcs.size === 0) { + throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + } + const ssrcToRtxSsrc = new Map(); + // First assume RTX is used. + for (const line of offerMediaObject.ssrcGroups || []) { + if (line.semantics !== 'FID') { + continue; + } + let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); + ssrc = Number(ssrc); + rtxSsrc = Number(rtxSsrc); + if (ssrcs.has(ssrc)) { + // Remove both the SSRC and RTX SSRC from the set so later we know that they + // are already handled. + ssrcs.delete(ssrc); + ssrcs.delete(rtxSsrc); + // Add to the map. + ssrcToRtxSsrc.set(ssrc, rtxSsrc); + } + } + // If the set of SSRCs is not empty it means that RTX is not being used, so take + // media SSRCs from there. + for (const ssrc of ssrcs) { + // Add to the map. + ssrcToRtxSsrc.set(ssrc, null); + } + const encodings = []; + for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { + const encoding = { ssrc }; + if (rtxSsrc) { + encoding.rtx = { ssrc: rtxSsrc }; + } + encodings.push(encoding); + } + return encodings; +} +planBUtils.getRtpEncodings = getRtpEncodings; +/** + * Adds multi-ssrc based simulcast into the given SDP media section offer. + */ +function addLegacySimulcast({ offerMediaObject, track, numStreams }) { + if (numStreams <= 1) { + throw new TypeError('numStreams must be greater than 1'); + } + let firstSsrc; + let firstRtxSsrc; + let streamId; + // Get the SSRC. + const ssrcMsidLine = (offerMediaObject.ssrcs || []) + .find((line) => { + if (line.attribute !== 'msid') { + return false; + } + const trackId = line.value.split(' ')[1]; + if (trackId === track.id) { + firstSsrc = line.id; + streamId = line.value.split(' ')[0]; + return true; + } + else { + return false; + } + }); + if (!ssrcMsidLine) { + throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + } + // Get the SSRC for RTX. + (offerMediaObject.ssrcGroups || []) + .some((line) => { + if (line.semantics !== 'FID') { + return false; + } + const ssrcs = line.ssrcs.split(/\s+/); + if (Number(ssrcs[0]) === firstSsrc) { + firstRtxSsrc = Number(ssrcs[1]); + return true; + } + else { + return false; + } + }); + const ssrcCnameLine = offerMediaObject.ssrcs + .find((line) => (line.attribute === 'cname' && line.id === firstSsrc)); + if (!ssrcCnameLine) { + throw new Error(`a=ssrc line with cname information not found [track.id:${track.id}]`); + } + const cname = ssrcCnameLine.value; + const ssrcs = []; + const rtxSsrcs = []; + for (let i = 0; i < numStreams; ++i) { + ssrcs.push(firstSsrc + i); + if (firstRtxSsrc) { + rtxSsrcs.push(firstRtxSsrc + i); + } + } + offerMediaObject.ssrcGroups = offerMediaObject.ssrcGroups || []; + offerMediaObject.ssrcs = offerMediaObject.ssrcs || []; + offerMediaObject.ssrcGroups.push({ + semantics: 'SIM', + ssrcs: ssrcs.join(' ') + }); + for (let i = 0; i < ssrcs.length; ++i) { + const ssrc = ssrcs[i]; + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId} ${track.id}` + }); + } + for (let i = 0; i < rtxSsrcs.length; ++i) { + const ssrc = ssrcs[i]; + const rtxSsrc = rtxSsrcs[i]; + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId} ${track.id}` + }); + offerMediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}` + }); + } +} +planBUtils.addLegacySimulcast = addLegacySimulcast; + +var __createBinding$9 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$9 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$9 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$9(result, mod, k); + __setModuleDefault$9(result, mod); + return result; +}; +Object.defineProperty(Chrome67$1, "__esModule", { value: true }); +Chrome67$1.Chrome67 = void 0; +const sdpTransform$6 = __importStar$9(lib); +const Logger_1$8 = Logger$3; +const utils$9 = __importStar$9(utils$h); +const ortc$8 = __importStar$9(ortc$d); +const sdpCommonUtils$6 = __importStar$9(commonUtils); +const sdpPlanBUtils$3 = __importStar$9(planBUtils); +const HandlerInterface_1$7 = HandlerInterface$1; +const RemoteSdp_1$6 = RemoteSdp$1; +const logger$8 = new Logger_1$8.Logger('Chrome67'); +const SCTP_NUM_STREAMS$6 = { OS: 1024, MIS: 1024 }; +class Chrome67 extends HandlerInterface_1$7.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome67(); + } + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of RTCRtpSender indexed by localId. + this._mapSendLocalIdRtpSender = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Chrome67'; + } + close() { + logger$8.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$8.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$6.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$6.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$8.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$6 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$8.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$6.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true + }); + this._sendingRtpParametersByKind = + { + audio: ortc$8.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$8.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$8.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$8.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$8.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + logger$8.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger$8.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$8.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$8.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); + logger$8.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger$8.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addTrack(track, this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$6.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils$9.clone(this._sendingRtpParametersByKind[track.kind]); + sendingRtpParameters.codecs = + ortc$8.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils$9.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + sendingRemoteRtpParameters.codecs = + ortc$8.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger$8.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform$6.parse(offer.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === 'video'); + sdpPlanBUtils$3.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform$6.write(localSdpObject) }; + } + logger$8.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform$6.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$6.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils$3.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if (sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + const rtpSender = this._pc.getSenders() + .find((s) => s.track === track); + // Insert into the map. + this._mapSendLocalIdRtpSender.set(localId, rtpSender); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + rtpSender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$8.debug('stopSending() [localId:%s]', localId); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + this._pc.removeTrack(rtpSender); + if (rtpSender.track) { + this._sendStream.removeTrack(rtpSender.track); + } + this._mapSendLocalIdRtpSender.delete(localId); + const offer = await this._pc.createOffer(); + logger$8.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger$8.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') { + return; + } + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger$8.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$8.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const oldTrack = rtpSender.track; + await rtpSender.replaceTrack(track); + // Remove the old track from the local stream. + if (oldTrack) { + this._sendStream.removeTrack(oldTrack); + } + // Add the new track to the local stream. + if (track) { + this._sendStream.addTrack(track); + } + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertSendDirection(); + logger$8.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this.assertSendDirection(); + logger$8.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + this.assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + return rtpSender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$8.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$6.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$6.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$8.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$8.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$6.parse(answer.sdp); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$6.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$6.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$8.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const localId = trackId; + const mid = kind; + const rtpReceiver = this._pc.getReceivers() + .find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) { + throw new Error('new RTCRtpReceiver not'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger$8.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$8.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async getReceiverStats(localId) { + this.assertRecvDirection(); + const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; + if (!rtpReceiver) { + throw new Error('associated RTCRtpReceiver not found'); + } + return rtpReceiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$8.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$8.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$6.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$8.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$6.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$6.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Chrome67$1.Chrome67 = Chrome67; + +var Chrome55$1 = {}; + +var __createBinding$8 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$8 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$8 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$8(result, mod, k); + __setModuleDefault$8(result, mod); + return result; +}; +Object.defineProperty(Chrome55$1, "__esModule", { value: true }); +Chrome55$1.Chrome55 = void 0; +const sdpTransform$5 = __importStar$8(lib); +const Logger_1$7 = Logger$3; +const errors_1$6 = errors; +const utils$8 = __importStar$8(utils$h); +const ortc$7 = __importStar$8(ortc$d); +const sdpCommonUtils$5 = __importStar$8(commonUtils); +const sdpPlanBUtils$2 = __importStar$8(planBUtils); +const HandlerInterface_1$6 = HandlerInterface$1; +const RemoteSdp_1$5 = RemoteSdp$1; +const logger$7 = new Logger_1$7.Logger('Chrome55'); +const SCTP_NUM_STREAMS$5 = { OS: 1024, MIS: 1024 }; +class Chrome55 extends HandlerInterface_1$6.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome55(); + } + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Chrome55'; + } + close() { + logger$7.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$7.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$5.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$5.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$7.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$5 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$7.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$5.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true + }); + this._sendingRtpParametersByKind = + { + audio: ortc$7.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$7.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$7.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$7.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$7.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + logger$7.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger$7.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$7.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$7.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); + logger$7.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger$7.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$5.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils$8.clone(this._sendingRtpParametersByKind[track.kind]); + sendingRtpParameters.codecs = + ortc$7.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils$8.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + sendingRemoteRtpParameters.codecs = + ortc$7.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger$7.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform$5.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils$2.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform$5.write(localSdpObject) }; + } + logger$7.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform$5.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$5.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils$2.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if (sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); + return { + localId: localId, + rtpParameters: sendingRtpParameters + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$7.debug('stopSending() [localId:%s]', localId); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) { + throw new Error('track not found'); + } + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); + const offer = await this._pc.createOffer(); + logger$7.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger$7.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') { + return; + } + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, track) { + throw new errors_1$6.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setMaxSpatialLayer(localId, spatialLayer) { + throw new errors_1$6.UnsupportedError(' not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setRtpEncodingParameters(localId, params) { + throw new errors_1$6.UnsupportedError('not supported'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSenderStats(localId) { + throw new errors_1$6.UnsupportedError('not implemented'); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$7.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$5.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$5.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$7.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$7.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$5.parse(answer.sdp); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$5.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$5.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$7.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const mid = kind; + const localId = trackId; + const streamId = options.streamId || rtpParameters.rtcp.cname; + const stream = this._pc.getRemoteStreams() + .find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) { + throw new Error('remote track not found'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + results.push({ localId, track }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger$7.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$7.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getReceiverStats(localId) { + throw new errors_1$6.UnsupportedError('not implemented'); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$7.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$7.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$5.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$7.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$5.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$5.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Chrome55$1.Chrome55 = Chrome55; + +var Firefox60$1 = {}; + +var __createBinding$7 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$7 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$7 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$7(result, mod, k); + __setModuleDefault$7(result, mod); + return result; +}; +Object.defineProperty(Firefox60$1, "__esModule", { value: true }); +Firefox60$1.Firefox60 = void 0; +const sdpTransform$4 = __importStar$7(lib); +const Logger_1$6 = Logger$3; +const errors_1$5 = errors; +const utils$7 = __importStar$7(utils$h); +const ortc$6 = __importStar$7(ortc$d); +const sdpCommonUtils$4 = __importStar$7(commonUtils); +const sdpUnifiedPlanUtils$2 = __importStar$7(unifiedPlanUtils); +const HandlerInterface_1$5 = HandlerInterface$1; +const RemoteSdp_1$4 = RemoteSdp$1; +const scalabilityModes_1$2 = scalabilityModes; +const logger$6 = new Logger_1$6.Logger('Firefox60'); +const SCTP_NUM_STREAMS$4 = { OS: 16, MIS: 2048 }; +class Firefox60 extends HandlerInterface_1$5.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Firefox60(); + } + constructor() { + super(); + // Closed flag. + this._closed = false; + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Firefox60'; + } + close() { + logger$6.debug('close()'); + if (this._closed) { + return; + } + this._closed = true; + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$6.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require' + }); + // NOTE: We need to add a real video track to get the RID extension mapping. + const canvas = document.createElement('canvas'); + // NOTE: Otherwise Firefox fails in next line. + canvas.getContext('2d'); + const fakeStream = canvas.captureStream(); + const fakeVideoTrack = fakeStream.getVideoTracks()[0]; + try { + pc.addTransceiver('audio', { direction: 'sendrecv' }); + const videoTransceiver = pc.addTransceiver(fakeVideoTrack, { direction: 'sendrecv' }); + const parameters = videoTransceiver.sender.getParameters(); + const encodings = [ + { rid: 'r0', maxBitrate: 100000 }, + { rid: 'r1', maxBitrate: 500000 } + ]; + parameters.encodings = encodings; + await videoTransceiver.sender.setParameters(parameters); + const offer = await pc.createOffer(); + try { + canvas.remove(); + } + catch (error) { } + try { + fakeVideoTrack.stop(); + } + catch (error) { } + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$4.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$4.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + canvas.remove(); + } + catch (error2) { } + try { + fakeVideoTrack.stop(); + } + catch (error2) { } + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$6.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$4 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + this.assertNotClosed(); + logger$6.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$4.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$6.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$6.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$6.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$6.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$6.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateIceServers(iceServers) { + this.assertNotClosed(); + // NOTE: Firefox does not implement pc.setConfiguration(). + throw new errors_1$5.UnsupportedError('not supported'); + } + async restartIce(iceParameters) { + this.assertNotClosed(); + logger$6.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$6.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$6.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + this.assertNotClosed(); + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$6.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings) { + encodings = utils$7.clone(encodings); + if (encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + // Clone the encodings and reverse them because Firefox likes them + // from high to low. + encodings.reverse(); + } + } + const sendingRtpParameters = utils$7.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$6.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$7.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$6.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + // NOTE: Firefox fails sometimes to properly anticipate the closed media + // section that it should use, so don't reuse closed media sections. + // https://github.com/versatica/mediasoup-client/issues/104 + // + // const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); + // NOTE: This is not spec compliants. Encodings should be given in addTransceiver + // second argument, but Firefox does not support it. + if (encodings) { + const parameters = transceiver.sender.getParameters(); + parameters.encodings = encodings; + await transceiver.sender.setParameters(parameters); + } + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$4.parse(offer.sdp); + // In Firefox use DTLS role client even if we are the "offerer" since + // Firefox does not respect ICE-Lite. + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + const layers = (0, scalabilityModes_1$2.parse)((encodings || [{}])[0].scalabilityMode); + logger$6.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$4.parse(this._pc.localDescription.sdp); + const offerMediaObject = localSdpObject.media[localSdpObject.media.length - 1]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$4.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils$2.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + const newEncodings = sdpUnifiedPlanUtils$2.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim (but + // reverse them back since we reversed them above to satisfy Firefox). + else { + sendingRtpParameters.encodings = encodings.reverse(); + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$6.debug('stopSending() [localId:%s]', localId); + if (this._closed) { + return; + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated transceiver not found'); + } + transceiver.sender.replaceTrack(null); + // NOTE: Cannot use stop() the transceiver due to the the note above in + // send() method. + // try + // { + // transceiver.stop(); + // } + // catch (error) + // {} + this._pc.removeTrack(transceiver.sender); + // NOTE: Cannot use closeMediaSection() due to the the note above in send() + // method. + // this._remoteSdp!.closeMediaSection(transceiver.mid); + this._remoteSdp.disableMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger$6.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$6.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$6.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$6.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + this._remoteSdp.resumeSendingMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$6.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertNotClosed(); + this.assertSendDirection(); + if (track) { + logger$6.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$6.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$6.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated transceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + // NOTE: We require encodings given from low to high, however Firefox + // requires them in reverse order, so do magic here. + spatialLayer = parameters.encodings.length - 1 - spatialLayer; + parameters.encodings.forEach((encoding, idx) => { + if (idx >= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$6.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$6.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$6.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertNotClosed(); + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$6.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$4.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$4.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger$6.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + optionsList) { + this.assertNotClosed(); + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$6.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$4.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$4.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + answer = { type: 'answer', sdp: sdpTransform$4.write(localSdpObject) }; + } + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger$6.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + if (this._closed) { + return; + } + for (const localId of localIds) { + logger$6.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$6.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$6.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$6.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$6.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$6.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertNotClosed(); + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$6.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$6.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$4.parse(answer.sdp); + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger$6.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$4.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$4.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertNotClosed() { + if (this._closed) { + throw new errors_1$5.InvalidStateError('method called in a closed handler'); + } + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Firefox60$1.Firefox60 = Firefox60; + +var Safari12$1 = {}; + +var __createBinding$6 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$6 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$6 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$6(result, mod, k); + __setModuleDefault$6(result, mod); + return result; +}; +Object.defineProperty(Safari12$1, "__esModule", { value: true }); +Safari12$1.Safari12 = void 0; +const sdpTransform$3 = __importStar$6(lib); +const Logger_1$5 = Logger$3; +const utils$6 = __importStar$6(utils$h); +const ortc$5 = __importStar$6(ortc$d); +const sdpCommonUtils$3 = __importStar$6(commonUtils); +const sdpUnifiedPlanUtils$1 = __importStar$6(unifiedPlanUtils); +const ortcUtils$1 = __importStar$6(utils$e); +const errors_1$4 = errors; +const HandlerInterface_1$4 = HandlerInterface$1; +const RemoteSdp_1$3 = RemoteSdp$1; +const scalabilityModes_1$1 = scalabilityModes; +const logger$5 = new Logger_1$5.Logger('Safari12'); +const SCTP_NUM_STREAMS$3 = { OS: 1024, MIS: 1024 }; +class Safari12 extends HandlerInterface_1$4.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari12(); + } + constructor() { + super(); + // Closed flag. + this._closed = false; + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Safari12'; + } + close() { + logger$5.debug('close()'); + if (this._closed) { + return; + } + this._closed = true; + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$5.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require' + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$3.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$3.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils$1.addNackSuppportForOpus(nativeRtpCapabilities); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$5.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$3 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + this.assertNotClosed(); + logger$5.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$3.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$5.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$5.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$5.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$5.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$5.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + this.assertNotClosed(); + logger$5.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + this.assertNotClosed(); + logger$5.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$5.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$5.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + this.assertNotClosed(); + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$5.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + const sendingRtpParameters = utils$6.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$5.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$6.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$5.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$3.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + const layers = (0, scalabilityModes_1$1.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && encodings.length > 1) { + logger$5.debug('send() | enabling legacy simulcast'); + localSdpObject = sdpTransform$3.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils$1.addLegacySimulcast({ + offerMediaObject, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform$3.write(localSdpObject) }; + } + logger$5.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$3.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$3.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils$1.getRtpEncodings({ offerMediaObject }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + if (this._closed) { + return; + } + logger$5.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger$5.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$5.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$5.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$5.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + this._remoteSdp.resumeSendingMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$5.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertNotClosed(); + this.assertSendDirection(); + if (track) { + logger$5.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$5.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$5.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$5.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$5.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$5.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertNotClosed(); + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$5.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$3.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$3.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$5.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertNotClosed(); + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$5.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$3.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$3.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$3.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$5.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + if (this._closed) { + return; + } + for (const localId of localIds) { + logger$5.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$5.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$5.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$5.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$5.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$5.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertNotClosed(); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertNotClosed(); + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$5.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$5.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$3.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$5.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$3.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$3.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertNotClosed() { + if (this._closed) { + throw new errors_1$4.InvalidStateError('method called in a closed handler'); + } + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Safari12$1.Safari12 = Safari12; + +var Safari11$1 = {}; + +var __createBinding$5 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$5 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$5 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$5(result, mod, k); + __setModuleDefault$5(result, mod); + return result; +}; +Object.defineProperty(Safari11$1, "__esModule", { value: true }); +Safari11$1.Safari11 = void 0; +const sdpTransform$2 = __importStar$5(lib); +const Logger_1$4 = Logger$3; +const utils$5 = __importStar$5(utils$h); +const ortc$4 = __importStar$5(ortc$d); +const sdpCommonUtils$2 = __importStar$5(commonUtils); +const sdpPlanBUtils$1 = __importStar$5(planBUtils); +const HandlerInterface_1$3 = HandlerInterface$1; +const RemoteSdp_1$2 = RemoteSdp$1; +const logger$4 = new Logger_1$4.Logger('Safari11'); +const SCTP_NUM_STREAMS$2 = { OS: 1024, MIS: 1024 }; +class Safari11 extends HandlerInterface_1$3.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari11(); + } + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of RTCRtpSender indexed by localId. + this._mapSendLocalIdRtpSender = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Safari11'; + } + close() { + logger$4.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$4.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$2.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$2.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$4.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$2 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$4.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$2.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true + }); + this._sendingRtpParametersByKind = + { + audio: ortc$4.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$4.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$4.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$4.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$4.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + logger$4.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger$4.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$4.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$4.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); + logger$4.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger$4.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addTrack(track, this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$2.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils$5.clone(this._sendingRtpParametersByKind[track.kind]); + sendingRtpParameters.codecs = + ortc$4.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils$5.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + sendingRemoteRtpParameters.codecs = + ortc$4.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger$4.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform$2.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils$1.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform$2.write(localSdpObject) }; + } + logger$4.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform$2.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$2.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils$1.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if (sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + const rtpSender = this._pc.getSenders() + .find((s) => s.track === track); + // Insert into the map. + this._mapSendLocalIdRtpSender.set(localId, rtpSender); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + rtpSender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + if (rtpSender.track) { + this._sendStream.removeTrack(rtpSender.track); + } + this._mapSendLocalIdRtpSender.delete(localId); + const offer = await this._pc.createOffer(); + logger$4.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger$4.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') { + return; + } + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger$4.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$4.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const oldTrack = rtpSender.track; + await rtpSender.replaceTrack(track); + // Remove the old track from the local stream. + if (oldTrack) { + this._sendStream.removeTrack(oldTrack); + } + // Add the new track to the local stream. + if (track) { + this._sendStream.addTrack(track); + } + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertSendDirection(); + logger$4.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this.assertSendDirection(); + logger$4.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + this.assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + return rtpSender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$4.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$2.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$2.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$4.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$4.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$2.parse(answer.sdp); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$2.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$2.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$4.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const mid = kind; + const localId = trackId; + const rtpReceiver = this._pc.getReceivers() + .find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) { + throw new Error('new RTCRtpReceiver not'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger$4.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$4.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertRecvDirection(); + const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; + if (!rtpReceiver) { + throw new Error('associated RTCRtpReceiver not found'); + } + return rtpReceiver.getStats(); + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$4.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$4.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$2.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$4.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$2.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$2.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +Safari11$1.Safari11 = Safari11; + +var Edge11$1 = {}; + +var edgeUtils$1 = {}; + +var __createBinding$4 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$4 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$4 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$4(result, mod, k); + __setModuleDefault$4(result, mod); + return result; +}; +Object.defineProperty(edgeUtils$1, "__esModule", { value: true }); +edgeUtils$1.mangleRtpParameters = edgeUtils$1.getCapabilities = void 0; +const utils$4 = __importStar$4(utils$h); +/** + * Normalize ORTC based Edge's RTCRtpReceiver.getCapabilities() to produce a full + * compliant ORTC RTCRtpCapabilities. + */ +function getCapabilities() { + const nativeCaps = RTCRtpReceiver.getCapabilities(); + const caps = utils$4.clone(nativeCaps); + for (const codec of caps.codecs ?? []) { + // Rename numChannels to channels. + // @ts-ignore + codec.channels = codec.numChannels; + // @ts-ignore + delete codec.numChannels; + // Add mimeType. + // @ts-ignore (due to codec.name). + codec.mimeType = codec.mimeType || `${codec.kind}/${codec.name}`; + // NOTE: Edge sets some numeric parameters as string rather than number. Fix them. + if (codec.parameters) { + const parameters = codec.parameters; + if (parameters.apt) { + parameters.apt = Number(parameters.apt); + } + if (parameters['packetization-mode']) { + parameters['packetization-mode'] = Number(parameters['packetization-mode']); + } + } + // Delete emty parameter String in rtcpFeedback. + for (const feedback of codec.rtcpFeedback || []) { + if (!feedback.parameter) { + feedback.parameter = ''; + } + } + } + return caps; +} +edgeUtils$1.getCapabilities = getCapabilities; +/** + * Generate RTCRtpParameters as ORTC based Edge likes. + */ +function mangleRtpParameters(rtpParameters) { + const params = utils$4.clone(rtpParameters); + // Rename mid to muxId. + if (params.mid) { + // @ts-ignore (due to muxId). + params.muxId = params.mid; + delete params.mid; + } + for (const codec of params.codecs) { + // Rename channels to numChannels. + if (codec.channels) { + // @ts-ignore. + codec.numChannels = codec.channels; + delete codec.channels; + } + // Add codec.name (requried by Edge). + // @ts-ignore (due to name). + if (codec.mimeType && !codec.name) { + // @ts-ignore (due to name). + codec.name = codec.mimeType.split('/')[1]; + } + // Remove mimeType. + // @ts-ignore + delete codec.mimeType; + } + return params; +} +edgeUtils$1.mangleRtpParameters = mangleRtpParameters; + +var __createBinding$3 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$3 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$3 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$3(result, mod, k); + __setModuleDefault$3(result, mod); + return result; +}; +Object.defineProperty(Edge11$1, "__esModule", { value: true }); +Edge11$1.Edge11 = void 0; +const Logger_1$3 = Logger$3; +const errors_1$3 = errors; +const utils$3 = __importStar$3(utils$h); +const ortc$3 = __importStar$3(ortc$d); +const edgeUtils = __importStar$3(edgeUtils$1); +const HandlerInterface_1$2 = HandlerInterface$1; +const logger$3 = new Logger_1$3.Logger('Edge11'); +class Edge11 extends HandlerInterface_1$2.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Edge11(); + } + constructor() { + super(); + // Map of RTCRtpSenders indexed by id. + this._rtpSenders = new Map(); + // Map of RTCRtpReceivers indexed by id. + this._rtpReceivers = new Map(); + // Next localId for sending tracks. + this._nextSendLocalId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'Edge11'; + } + close() { + logger$3.debug('close()'); + // Close the ICE gatherer. + // NOTE: Not yet implemented by Edge. + try { + this._iceGatherer.close(); + } + catch (error) { } + // Close the ICE transport. + try { + this._iceTransport.stop(); + } + catch (error) { } + // Close the DTLS transport. + try { + this._dtlsTransport.stop(); + } + catch (error) { } + // Close RTCRtpSenders. + for (const rtpSender of this._rtpSenders.values()) { + try { + rtpSender.stop(); + } + catch (error) { } + } + // Close RTCRtpReceivers. + for (const rtpReceiver of this._rtpReceivers.values()) { + try { + rtpReceiver.stop(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$3.debug('getNativeRtpCapabilities()'); + return edgeUtils.getCapabilities(); + } + async getNativeSctpCapabilities() { + logger$3.debug('getNativeSctpCapabilities()'); + return { + numStreams: { OS: 0, MIS: 0 } + }; + } + run({ direction, // eslint-disable-line @typescript-eslint/no-unused-vars + iceParameters, iceCandidates, dtlsParameters, sctpParameters, // eslint-disable-line @typescript-eslint/no-unused-vars + iceServers, iceTransportPolicy, additionalSettings, // eslint-disable-line @typescript-eslint/no-unused-vars + proprietaryConstraints, // eslint-disable-line @typescript-eslint/no-unused-vars + extendedRtpCapabilities }) { + logger$3.debug('run()'); + this._sendingRtpParametersByKind = + { + audio: ortc$3.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$3.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._remoteIceParameters = iceParameters; + this._remoteIceCandidates = iceCandidates; + this._remoteDtlsParameters = dtlsParameters; + this._cname = `CNAME-${utils$3.generateRandomNumber()}`; + this.setIceGatherer({ iceServers, iceTransportPolicy }); + this.setIceTransport(); + this.setDtlsTransport(); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateIceServers(iceServers) { + // NOTE: Edge 11 does not implement iceGatherer.gater(). + throw new errors_1$3.UnsupportedError('not supported'); + } + async restartIce(iceParameters) { + logger$3.debug('restartIce()'); + this._remoteIceParameters = iceParameters; + if (!this._transportReady) { + return; + } + logger$3.debug('restartIce() | calling iceTransport.start()'); + this._iceTransport.start(this._iceGatherer, iceParameters, 'controlling'); + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); + } + this._iceTransport.addRemoteCandidate({}); + } + async getTransportStats() { + return this._iceTransport.getStats(); + } + async send( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { track, encodings, codecOptions, codec }) { + logger$3.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'server' }); + } + logger$3.debug('send() | calling new RTCRtpSender()'); + const rtpSender = new RTCRtpSender(track, this._dtlsTransport); + const rtpParameters = utils$3.clone(this._sendingRtpParametersByKind[track.kind]); + rtpParameters.codecs = ortc$3.reduceCodecs(rtpParameters.codecs, codec); + const useRtx = rtpParameters.codecs + .some((_codec) => /.+\/rtx$/i.test(_codec.mimeType)); + if (!encodings) { + encodings = [{}]; + } + for (const encoding of encodings) { + encoding.ssrc = utils$3.generateRandomNumber(); + if (useRtx) { + encoding.rtx = { ssrc: utils$3.generateRandomNumber() }; + } + } + rtpParameters.encodings = encodings; + // Fill RTCRtpParameters.rtcp. + rtpParameters.rtcp = + { + cname: this._cname, + reducedSize: true, + mux: true + }; + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger$3.debug('send() | calling rtpSender.send() [params:%o]', edgeRtpParameters); + await rtpSender.send(edgeRtpParameters); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Store it. + this._rtpSenders.set(localId, rtpSender); + return { localId, rtpParameters, rtpSender }; + } + async stopSending(localId) { + logger$3.debug('stopSending() [localId:%s]', localId); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + this._rtpSenders.delete(localId); + try { + logger$3.debug('stopSending() | calling rtpSender.stop()'); + rtpSender.stop(); + } + catch (error) { + logger$3.warn('stopSending() | rtpSender.stop() failed:%o', error); + throw error; + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack(localId, track) { + if (track) { + logger$3.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$3.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + rtpSender.setTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + logger$3.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings + .forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + logger$3.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + return rtpSender.getStats(); + } + async sendDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options) { + throw new errors_1$3.UnsupportedError('not implemented'); + } + async receive(optionsList) { + const results = []; + for (const options of optionsList) { + const { trackId, kind } = options; + logger$3.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + } + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'server' }); + } + for (const options of optionsList) { + const { trackId, kind, rtpParameters } = options; + logger$3.debug('receive() | calling new RTCRtpReceiver()'); + const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); + rtpReceiver.addEventListener('error', (event) => { + logger$3.error('rtpReceiver "error" event [event:%o]', event); + }); + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger$3.debug('receive() | calling rtpReceiver.receive() [params:%o]', edgeRtpParameters); + await rtpReceiver.receive(edgeRtpParameters); + const localId = trackId; + // Store it. + this._rtpReceivers.set(localId, rtpReceiver); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; + } + async stopReceiving(localIds) { + for (const localId of localIds) { + logger$3.debug('stopReceiving() [localId:%s]', localId); + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) { + throw new Error('RTCRtpReceiver not found'); + } + this._rtpReceivers.delete(localId); + try { + logger$3.debug('stopReceiving() | calling rtpReceiver.stop()'); + rtpReceiver.stop(); + } + catch (error) { + logger$3.warn('stopReceiving() | rtpReceiver.stop() failed:%o', error); + } + } + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async getReceiverStats(localId) { + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) { + throw new Error('RTCRtpReceiver not found'); + } + return rtpReceiver.getStats(); + } + async receiveDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options) { + throw new errors_1$3.UnsupportedError('not implemented'); + } + setIceGatherer({ iceServers, iceTransportPolicy }) { + // @ts-ignore + const iceGatherer = new RTCIceGatherer({ + iceServers: iceServers || [], + gatherPolicy: iceTransportPolicy || 'all' + }); + iceGatherer.addEventListener('error', (event) => { + logger$3.error('iceGatherer "error" event [event:%o]', event); + }); + // NOTE: Not yet implemented by Edge, which starts gathering automatically. + try { + iceGatherer.gather(); + } + catch (error) { + logger$3.debug('setIceGatherer() | iceGatherer.gather() failed: %s', error.toString()); + } + this._iceGatherer = iceGatherer; + } + setIceTransport() { + const iceTransport = new RTCIceTransport(this._iceGatherer); + // NOTE: Not yet implemented by Edge. + iceTransport.addEventListener('statechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + // NOTE: Not standard, but implemented by Edge. + iceTransport.addEventListener('icestatechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + iceTransport.addEventListener('candidatepairchange', (event) => { + logger$3.debug('iceTransport "candidatepairchange" event [pair:%o]', event.pair); + }); + this._iceTransport = iceTransport; + } + setDtlsTransport() { + const dtlsTransport = new RTCDtlsTransport(this._iceTransport); + // NOTE: Not yet implemented by Edge. + dtlsTransport.addEventListener('statechange', () => { + logger$3.debug('dtlsTransport "statechange" event [state:%s]', dtlsTransport.state); + }); + // NOTE: Not standard, but implemented by Edge. + dtlsTransport.addEventListener('dtlsstatechange', () => { + logger$3.debug('dtlsTransport "dtlsstatechange" event [state:%s]', dtlsTransport.state); + if (dtlsTransport.state === 'closed') { + this.emit('@connectionstatechange', 'closed'); + } + }); + dtlsTransport.addEventListener('error', (event) => { + logger$3.error('dtlsTransport "error" event [event:%o]', event); + }); + this._dtlsTransport = dtlsTransport; + } + async setupTransport({ localDtlsRole }) { + logger$3.debug('setupTransport()'); + // Get our local DTLS parameters. + const dtlsParameters = this._dtlsTransport.getLocalParameters(); + dtlsParameters.role = localDtlsRole; + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + // Start the RTCIceTransport. + this._iceTransport.start(this._iceGatherer, this._remoteIceParameters, 'controlling'); + // Add remote ICE candidates. + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); + } + // Also signal a 'complete' candidate as per spec. + // NOTE: It should be {complete: true} but Edge prefers {}. + // NOTE: If we don't signal end of candidates, the Edge RTCIceTransport + // won't enter the 'completed' state. + this._iceTransport.addRemoteCandidate({}); + // NOTE: Edge does not like SHA less than 256. + this._remoteDtlsParameters.fingerprints = this._remoteDtlsParameters.fingerprints + .filter((fingerprint) => { + return (fingerprint.algorithm === 'sha-256' || + fingerprint.algorithm === 'sha-384' || + fingerprint.algorithm === 'sha-512'); + }); + // Start the RTCDtlsTransport. + this._dtlsTransport.start(this._remoteDtlsParameters); + this._transportReady = true; + } +} +Edge11$1.Edge11 = Edge11; + +var ReactNativeUnifiedPlan$1 = {}; + +var __createBinding$2 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$2 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$2 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$2(result, mod, k); + __setModuleDefault$2(result, mod); + return result; +}; +Object.defineProperty(ReactNativeUnifiedPlan$1, "__esModule", { value: true }); +ReactNativeUnifiedPlan$1.ReactNativeUnifiedPlan = void 0; +const sdpTransform$1 = __importStar$2(lib); +const Logger_1$2 = Logger$3; +const utils$2 = __importStar$2(utils$h); +const ortc$2 = __importStar$2(ortc$d); +const sdpCommonUtils$1 = __importStar$2(commonUtils); +const sdpUnifiedPlanUtils = __importStar$2(unifiedPlanUtils); +const ortcUtils = __importStar$2(utils$e); +const errors_1$2 = errors; +const HandlerInterface_1$1 = HandlerInterface$1; +const RemoteSdp_1$1 = RemoteSdp$1; +const scalabilityModes_1 = scalabilityModes; +const logger$2 = new Logger_1$2.Logger('ReactNativeUnifiedPlan'); +const SCTP_NUM_STREAMS$1 = { OS: 1024, MIS: 1024 }; +class ReactNativeUnifiedPlan extends HandlerInterface_1$1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new ReactNativeUnifiedPlan(); + } + constructor() { + super(); + // Closed flag. + this._closed = false; + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'ReactNativeUnifiedPlan'; + } + close() { + logger$2.debug('close()'); + if (this._closed) { + return; + } + this._closed = true; + // Free/dispose native MediaStream but DO NOT free/dispose native + // MediaStreamTracks (that is parent's business). + // @ts-ignore (proprietary API in react-native-webrtc). + this._sendStream.release(/* releaseTracks */ false); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$2.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan' + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform$1.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils$1.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils.addNackSuppportForOpus(nativeRtpCapabilities); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$2.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS$1 + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + this.assertNotClosed(); + logger$2.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1$1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); + this._sendingRtpParametersByKind = + { + audio: ortc$2.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$2.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$2.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$2.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$2.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + this.assertNotClosed(); + logger$2.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + this.assertNotClosed(); + logger$2.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$2.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$2.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + this.assertNotClosed(); + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$2.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + } + const sendingRtpParameters = utils$2.clone(this._sendingRtpParametersByKind[track.kind]); + // This may throw. + sendingRtpParameters.codecs = + ortc$2.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils$2.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc$2.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform$1.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { + logger$2.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; + localSdpObject = sdpTransform$1.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: layers.spatialLayers + }); + offer = { type: 'offer', sdp: sdpTransform$1.write(localSdpObject) }; + } + logger$2.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + // NOTE: We cannot read generated MID on iOS react-native-webrtc 111.0.0 + // because transceiver.mid is not available until setRemoteDescription() + // is called, so this is best effort. + // Issue: https://github.com/react-native-webrtc/react-native-webrtc/issues/1404 + // NOTE: So let's fill MID in sendingRtpParameters later. + // NOTE: This is fixed in react-native-webrtc 111.0.3. + let localId = transceiver.mid ?? undefined; + if (!localId) { + logger$2.warn('send() | missing transceiver.mid (bug in react-native-webrtc, using a workaround'); + } + // Set MID. + // NOTE: As per above, it could be unset yet. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform$1.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils$1.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + // Hack for VP9 SVC. + if (hackVp9Svc) { + newEncodings = [newEncodings[0]]; + } + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Follow up of iOS react-native-webrtc 111.0.0 issue told above. Now yes, + // we can read generated MID (if not done above) and fill sendingRtpParameters. + // NOTE: This is fixed in react-native-webrtc 111.0.3 so this block isn't + // needed starting from that version. + if (!localId) { + localId = transceiver.mid; + sendingRtpParameters.mid = localId; + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + if (this._closed) { + return; + } + logger$2.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger$2.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + async pauseSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$2.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger$2.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async resumeSending(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$2.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger$2.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertNotClosed(); + this.assertSendDirection(); + if (track) { + logger$2.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger$2.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$2.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$2.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async setRtpEncodingParameters(localId, params) { + this.assertNotClosed(); + this.assertSendDirection(); + logger$2.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger$2.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async getSenderStats(localId) { + this.assertNotClosed(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertNotClosed(); + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$2.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS$1.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform$1.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$2.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertNotClosed(); + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger$2.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform$1.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils$1.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform$1.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$2.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + if (this._closed) { + return; + } + for (const localId of localIds) { + logger$2.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$2.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$2.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$2.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertNotClosed(); + this.assertRecvDirection(); + for (const localId of localIds) { + logger$2.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$2.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertNotClosed(); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertNotClosed(); + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$2.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$2.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform$1.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$2.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform$1.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils$1.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertNotClosed() { + if (this._closed) { + throw new errors_1$2.InvalidStateError('method called in a closed handler'); + } + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +ReactNativeUnifiedPlan$1.ReactNativeUnifiedPlan = ReactNativeUnifiedPlan; + +var ReactNative$1 = {}; + +var __createBinding$1 = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault$1 = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar$1 = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding$1(result, mod, k); + __setModuleDefault$1(result, mod); + return result; +}; +Object.defineProperty(ReactNative$1, "__esModule", { value: true }); +ReactNative$1.ReactNative = void 0; +const sdpTransform = __importStar$1(lib); +const Logger_1$1 = Logger$3; +const errors_1$1 = errors; +const utils$1 = __importStar$1(utils$h); +const ortc$1 = __importStar$1(ortc$d); +const sdpCommonUtils = __importStar$1(commonUtils); +const sdpPlanBUtils = __importStar$1(planBUtils); +const HandlerInterface_1 = HandlerInterface$1; +const RemoteSdp_1 = RemoteSdp$1; +const logger$1 = new Logger_1$1.Logger('ReactNative'); +const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; +class ReactNative extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new ReactNative(); + } + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + get name() { + return 'ReactNative'; + } + close() { + logger$1.debug('close()'); + // Free/dispose native MediaStream but DO NOT free/dispose native + // MediaStreamTracks (that is parent's business). + // @ts-ignore (proprietary API in react-native-webrtc). + this._sendStream.release(/* releaseTracks */ false); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger$1.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; + } + } + async getNativeSctpCapabilities() { + logger$1.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger$1.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true + }); + this._sendingRtpParametersByKind = + { + audio: ortc$1.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc$1.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc$1.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc$1.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + this._pc.addEventListener('icegatheringstatechange', () => { + this.emit('@icegatheringstatechange', this._pc.iceGatheringState); + }); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger$1.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + } + async updateIceServers(iceServers) { + logger$1.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger$1.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger$1.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$1.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); + logger$1.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger$1.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils$1.clone(this._sendingRtpParametersByKind[track.kind]); + sendingRtpParameters.codecs = + ortc$1.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils$1.clone(this._sendingRemoteRtpParametersByKind[track.kind]); + sendingRemoteRtpParameters.codecs = + ortc$1.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger$1.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger$1.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); + return { + localId: localId, + rtpParameters: sendingRtpParameters + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger$1.debug('stopSending() [localId:%s]', localId); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) { + throw new Error('track not found'); + } + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); + const offer = await this._pc.createOffer(); + logger$1.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger$1.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') { + return; + } + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, track) { + throw new errors_1$1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setMaxSpatialLayer(localId, spatialLayer) { + throw new errors_1$1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setRtpEncodingParameters(localId, params) { + throw new errors_1$1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSenderStats(localId) { + throw new errors_1$1.UnsupportedError('not implemented'); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$1.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$1.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; + } + async receive(optionsList) { + this.assertRecvDirection(); + const results = []; + const mapStreamId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters } = options; + logger$1.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + let streamId = options.streamId || rtpParameters.rtcp.cname; + // NOTE: In React-Native we cannot reuse the same remote MediaStream for new + // remote tracks. This is because react-native-webrtc does not react on new + // tracks generated within already existing streams, so force the streamId + // to be different. See: + // https://github.com/react-native-webrtc/react-native-webrtc/issues/401 + logger$1.debug('receive() | forcing a random remote streamId to avoid well known bug in react-native-webrtc'); + streamId += `-hack-${utils$1.generateRandomNumber()}`; + mapStreamId.set(trackId, streamId); + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$1.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const localId = trackId; + const mid = kind; + const streamId = mapStreamId.get(trackId); + const stream = this._pc.getRemoteStreams() + .find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) { + throw new Error('remote track not found'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + results.push({ localId, track }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger$1.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger$1.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getReceiverStats(localId) { + throw new errors_1$1.UnsupportedError('not implemented'); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger$1.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger$1.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: this._forcedLocalDtlsRole ?? 'client', + localSdpObject + }); + } + logger$1.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } +} +ReactNative$1.ReactNative = ReactNative; + +var __createBinding = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(Device$1, "__esModule", { value: true }); +Device$1.Device = Device$1.detectDevice = void 0; +const ua_parser_js_1 = uaParserExports; +const Logger_1 = Logger$3; +const EnhancedEventEmitter_1 = EnhancedEventEmitter$1; +const errors_1 = errors; +const utils = __importStar(utils$h); +const ortc = __importStar(ortc$d); +const Transport_1 = Transport$1; +const Chrome111_1 = Chrome111$1; +const Chrome74_1 = Chrome74$1; +const Chrome70_1 = Chrome70$1; +const Chrome67_1 = Chrome67$1; +const Chrome55_1 = Chrome55$1; +const Firefox60_1 = Firefox60$1; +const Safari12_1 = Safari12$1; +const Safari11_1 = Safari11$1; +const Edge11_1 = Edge11$1; +const ReactNativeUnifiedPlan_1 = ReactNativeUnifiedPlan$1; +const ReactNative_1 = ReactNative$1; +const logger = new Logger_1.Logger('Device'); +function detectDevice() { + // React-Native. + // NOTE: react-native-webrtc >= 1.75.0 is required. + // NOTE: react-native-webrtc with Unified Plan requires version >= 106.0.0. + if (typeof navigator === 'object' && navigator.product === 'ReactNative') { + logger.debug('detectDevice() | React-Native detected'); + if (typeof RTCPeerConnection === 'undefined') { + logger.warn('detectDevice() | unsupported react-native-webrtc without RTCPeerConnection, forgot to call registerGlobals()?'); + return undefined; + } + if (typeof RTCRtpTransceiver !== 'undefined') { + logger.debug('detectDevice() | ReactNative UnifiedPlan handler chosen'); + return 'ReactNativeUnifiedPlan'; + } + else { + logger.debug('detectDevice() | ReactNative PlanB handler chosen'); + return 'ReactNative'; + } + } + // Browser. + else if (typeof navigator === 'object' && typeof navigator.userAgent === 'string') { + const ua = navigator.userAgent; + const uaParser = new ua_parser_js_1.UAParser(ua); + logger.debug('detectDevice() | browser detected [ua:%s, parsed:%o]', ua, uaParser.getResult()); + const browser = uaParser.getBrowser(); + const browserName = browser.name?.toLowerCase(); + const browserVersion = parseInt(browser.major ?? '0'); + const engine = uaParser.getEngine(); + const engineName = engine.name?.toLowerCase(); + const os = uaParser.getOS(); + const osName = os.name?.toLowerCase(); + const osVersion = parseFloat(os.version ?? '0'); + const device = uaParser.getDevice(); + const deviceModel = device.model?.toLowerCase(); + const isIOS = osName === 'ios' || deviceModel === 'ipad'; + const isChrome = browserName && + [ + 'chrome', + 'chromium', + 'mobile chrome', + 'chrome webview', + 'chrome headless' + ].includes(browserName); + const isFirefox = browserName && + [ + 'firefox', + 'mobile firefox', + 'mobile focus' + ].includes(browserName); + const isSafari = browserName && + [ + 'safari', + 'mobile safari' + ].includes(browserName); + const isEdge = browserName && ['edge'].includes(browserName); + // Chrome, Chromium, and Edge. + if ((isChrome || isEdge) && !isIOS && browserVersion >= 111) { + return 'Chrome111'; + } + else if ((isChrome && !isIOS && browserVersion >= 74) || + (isEdge && !isIOS && browserVersion >= 88)) { + return 'Chrome74'; + } + else if (isChrome && !isIOS && browserVersion >= 70) { + return 'Chrome70'; + } + else if (isChrome && !isIOS && browserVersion >= 67) { + return 'Chrome67'; + } + else if (isChrome && !isIOS && browserVersion >= 55) { + return 'Chrome55'; + } + // Firefox. + else if (isFirefox && !isIOS && browserVersion >= 60) { + return 'Firefox60'; + } + // Firefox on iOS (so Safari). + else if (isFirefox && isIOS && osVersion >= 14.3) { + return 'Safari12'; + } + // Safari with Unified-Plan support enabled. + else if (isSafari && + browserVersion >= 12 && + typeof RTCRtpTransceiver !== 'undefined' && + RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) { + return 'Safari12'; + } + // Safari with Plab-B support. + else if (isSafari && browserVersion >= 11) { + return 'Safari11'; + } + // Old Edge with ORTC support. + else if (isEdge && !isIOS && browserVersion >= 11 && browserVersion <= 18) { + return 'Edge11'; + } + // Best effort for WebKit based browsers in iOS. + else if (engineName === 'webkit' && + isIOS && + typeof RTCRtpTransceiver !== 'undefined' && + RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) { + return 'Safari12'; + } + // Best effort for Chromium based browsers. + else if (engineName === 'blink') { + const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); + if (match) { + const version = Number(match[1]); + if (version >= 111) { + return 'Chrome111'; + } + else if (version >= 74) { + return 'Chrome74'; + } + else if (version >= 70) { + return 'Chrome70'; + } + else if (version >= 67) { + return 'Chrome67'; + } + else { + return 'Chrome55'; + } + } + else { + return 'Chrome111'; + } + } + // Unsupported browser. + else { + logger.warn('detectDevice() | browser not supported [name:%s, version:%s]', browserName, browserVersion); + return undefined; + } + } + // Unknown device. + else { + logger.warn('detectDevice() | unknown device'); + return undefined; + } +} +Device$1.detectDevice = detectDevice; +class Device { + /** + * Create a new Device to connect to mediasoup server. + * + * @throws {UnsupportedError} if device is not supported. + */ + constructor({ handlerName, handlerFactory, Handler } = {}) { + // Loaded flag. + this._loaded = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + // Handle deprecated option. + if (Handler) { + logger.warn('constructor() | Handler option is DEPRECATED, use handlerName or handlerFactory instead'); + if (typeof Handler === 'string') { + handlerName = Handler; + } + else { + throw new TypeError('non string Handler option no longer supported, use handlerFactory instead'); + } + } + if (handlerName && handlerFactory) { + throw new TypeError('just one of handlerName or handlerInterface can be given'); + } + if (handlerFactory) { + this._handlerFactory = handlerFactory; + } + else { + if (handlerName) { + logger.debug('constructor() | handler given: %s', handlerName); + } + else { + handlerName = detectDevice(); + if (handlerName) { + logger.debug('constructor() | detected handler: %s', handlerName); + } + else { + throw new errors_1.UnsupportedError('device not supported'); + } + } + switch (handlerName) { + case 'Chrome111': + this._handlerFactory = Chrome111_1.Chrome111.createFactory(); + break; + case 'Chrome74': + this._handlerFactory = Chrome74_1.Chrome74.createFactory(); + break; + case 'Chrome70': + this._handlerFactory = Chrome70_1.Chrome70.createFactory(); + break; + case 'Chrome67': + this._handlerFactory = Chrome67_1.Chrome67.createFactory(); + break; + case 'Chrome55': + this._handlerFactory = Chrome55_1.Chrome55.createFactory(); + break; + case 'Firefox60': + this._handlerFactory = Firefox60_1.Firefox60.createFactory(); + break; + case 'Safari12': + this._handlerFactory = Safari12_1.Safari12.createFactory(); + break; + case 'Safari11': + this._handlerFactory = Safari11_1.Safari11.createFactory(); + break; + case 'Edge11': + this._handlerFactory = Edge11_1.Edge11.createFactory(); + break; + case 'ReactNativeUnifiedPlan': + this._handlerFactory = ReactNativeUnifiedPlan_1.ReactNativeUnifiedPlan.createFactory(); + break; + case 'ReactNative': + this._handlerFactory = ReactNative_1.ReactNative.createFactory(); + break; + default: + throw new TypeError(`unknown handlerName "${handlerName}"`); + } + } + // Create a temporal handler to get its name. + const handler = this._handlerFactory(); + this._handlerName = handler.name; + handler.close(); + this._extendedRtpCapabilities = undefined; + this._recvRtpCapabilities = undefined; + this._canProduceByKind = + { + audio: false, + video: false + }; + this._sctpCapabilities = undefined; + } + /** + * The RTC handler name. + */ + get handlerName() { + return this._handlerName; + } + /** + * Whether the Device is loaded. + */ + get loaded() { + return this._loaded; + } + /** + * RTP capabilities of the Device for receiving media. + * + * @throws {InvalidStateError} if not loaded. + */ + get rtpCapabilities() { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + return this._recvRtpCapabilities; + } + /** + * SCTP capabilities of the Device. + * + * @throws {InvalidStateError} if not loaded. + */ + get sctpCapabilities() { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + return this._sctpCapabilities; + } + get observer() { + return this._observer; + } + /** + * Initialize the Device. + */ + async load({ routerRtpCapabilities }) { + logger.debug('load() [routerRtpCapabilities:%o]', routerRtpCapabilities); + routerRtpCapabilities = utils.clone(routerRtpCapabilities); + // Temporal handler to get its capabilities. + let handler; + try { + if (this._loaded) { + throw new errors_1.InvalidStateError('already loaded'); + } + // This may throw. + ortc.validateRtpCapabilities(routerRtpCapabilities); + handler = this._handlerFactory(); + const nativeRtpCapabilities = await handler.getNativeRtpCapabilities(); + logger.debug('load() | got native RTP capabilities:%o', nativeRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(nativeRtpCapabilities); + // Get extended RTP capabilities. + this._extendedRtpCapabilities = ortc.getExtendedRtpCapabilities(nativeRtpCapabilities, routerRtpCapabilities); + logger.debug('load() | got extended RTP capabilities:%o', this._extendedRtpCapabilities); + // Check whether we can produce audio/video. + this._canProduceByKind.audio = + ortc.canSend('audio', this._extendedRtpCapabilities); + this._canProduceByKind.video = + ortc.canSend('video', this._extendedRtpCapabilities); + // Generate our receiving RTP capabilities for receiving media. + this._recvRtpCapabilities = + ortc.getRecvRtpCapabilities(this._extendedRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(this._recvRtpCapabilities); + logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities); + // Generate our SCTP capabilities. + this._sctpCapabilities = await handler.getNativeSctpCapabilities(); + logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities); + // This may throw. + ortc.validateSctpCapabilities(this._sctpCapabilities); + logger.debug('load() succeeded'); + this._loaded = true; + handler.close(); + } + catch (error) { + if (handler) { + handler.close(); + } + throw error; + } + } + /** + * Whether we can produce audio/video. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + canProduce(kind) { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + else if (kind !== 'audio' && kind !== 'video') { + throw new TypeError(`invalid kind "${kind}"`); + } + return this._canProduceByKind[kind]; + } + /** + * Creates a Transport for sending media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createSendTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + logger.debug('createSendTransport()'); + return this.createTransport({ + direction: 'send', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData + }); + } + /** + * Creates a Transport for receiving media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createRecvTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + logger.debug('createRecvTransport()'); + return this.createTransport({ + direction: 'recv', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData + }); + } + createTransport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof iceParameters !== 'object') { + throw new TypeError('missing iceParameters'); + } + else if (!Array.isArray(iceCandidates)) { + throw new TypeError('missing iceCandidates'); + } + else if (typeof dtlsParameters !== 'object') { + throw new TypeError('missing dtlsParameters'); + } + else if (sctpParameters && typeof sctpParameters !== 'object') { + throw new TypeError('wrong sctpParameters'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Create a new Transport. + const transport = new Transport_1.Transport({ + direction, + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData, + handlerFactory: this._handlerFactory, + extendedRtpCapabilities: this._extendedRtpCapabilities, + canProduceByKind: this._canProduceByKind + }); + // Emit observer event. + this._observer.safeEmit('newtransport', transport); + return transport; + } +} +Device$1.Device = Device; + +var types = {}; + +var RtpParameters = {}; + +/** + * The RTP capabilities define what mediasoup or an endpoint can receive at + * media level. + */ +Object.defineProperty(RtpParameters, "__esModule", { value: true }); + +var SctpParameters = {}; + +Object.defineProperty(SctpParameters, "__esModule", { value: true }); + +(function (exports) { + var __createBinding = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); + }) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + })); + var __exportStar = (commonjsGlobal && commonjsGlobal.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); + }; + Object.defineProperty(exports, "__esModule", { value: true }); + __exportStar(Device$1, exports); + __exportStar(Transport$1, exports); + __exportStar(Producer$1, exports); + __exportStar(Consumer$1, exports); + __exportStar(DataProducer$1, exports); + __exportStar(DataConsumer$1, exports); + __exportStar(RtpParameters, exports); + __exportStar(SctpParameters, exports); + __exportStar(HandlerInterface$1, exports); + __exportStar(errors, exports); +} (types)); + +(function (exports) { + var __createBinding = (commonjsGlobal && commonjsGlobal.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); + }) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + })); + var __setModuleDefault = (commonjsGlobal && commonjsGlobal.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + }) : function(o, v) { + o["default"] = v; + }); + var __importStar = (commonjsGlobal && commonjsGlobal.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + var __importDefault = (commonjsGlobal && commonjsGlobal.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.debug = exports.parseScalabilityMode = exports.detectDevice = exports.Device = exports.version = exports.types = void 0; + const debug_1 = __importDefault(browserExports); + exports.debug = debug_1.default; + const Device_1 = Device$1; + Object.defineProperty(exports, "Device", { enumerable: true, get: function () { return Device_1.Device; } }); + Object.defineProperty(exports, "detectDevice", { enumerable: true, get: function () { return Device_1.detectDevice; } }); + const types$1 = __importStar(types); + exports.types = types$1; + /** + * Expose mediasoup-client version. + */ + exports.version = '3.7.0'; + /** + * Expose parseScalabilityMode() function. + */ + var scalabilityModes_1 = scalabilityModes; + Object.defineProperty(exports, "parseScalabilityMode", { enumerable: true, get: function () { return scalabilityModes_1.parse; } }); +} (lib$2)); + +/** + * @typedef Payload + * @property {Object} message + * @property {string} [needResponse] + * @property {string} [responseTo] + */ + +/** + * Bus class that implements a request/response pattern and a batching feature on top of a websocket. + * + * Compatible in both Node and Browser environment. + */ +class Bus { + /** + * Used to know if the code runs on the client to avoid ID collisions as this class can is defined in both + * the server(/node) and the client(/browser) environments (and therefore not share static properties). + * + * @type {"c"|"s"} + */ + static _type = + typeof window !== "undefined" && typeof window.document !== "undefined" ? "c" : "s"; + /** @type {number} */ + static _idCount = 0; + + /** @type {Function} */ + onMessage; + /** @type {Function} */ + onRequest; + /** @type {number} */ + id = Bus._idCount++; + /** @type {number} */ + _requestCount = 0; + /** @type {WebSocket} */ + _websocket; + /** + * Whether the websocket is an EventEmitter (from `ws` node library), if false it is a EventTarget like the native browser ws. + * @type {boolean} + */ + _isWebsocketEmitter; + /** @type {Map} */ + _pendingRequests = new Map(); + /** @type {Payload[]} */ + _messageQueue = []; + /** @type {NodeJS.Timeout | string} */ + _batchTimeout; + /** @type {number} */ + _batchDelay = 100; + + /** + * @param {import("ws").WebSocket} websocket + * @param {Object} [options] + * @param {number} [options.batchDelay] in milliseconds + */ + constructor(websocket, { batchDelay = 200 } = {}) { + this._batchDelay = batchDelay; + this._websocket = websocket; + this._isWebsocketEmitter = typeof websocket.on === "function"; // could use constructor name but can't use mocks during tests + this._onMessage = this._onMessage.bind(this); + this._onSocket("message", this._onMessage); + this._onSocket("close", () => { + this.close(); + }); + } + + close() { + clearTimeout(this._batchTimeout); + this.onMessage = null; + this.onRequest = null; + this._sendPayload = () => {}; + for (const { reject, timeout } of this._pendingRequests.values()) { + clearTimeout(timeout); + reject(new Error("bus closed")); + } + this._offSocket("message", this._onMessage); + } + + /** + * @param {Object} message + * @param {Object} [options] + * @param {number} [options.timeout] in milliseconds + * @param {boolean} [options.batch] + * @returns {Promise} response + */ + request(message, { timeout = 5000, batch } = {}) { + const requestId = this._getNextRequestId(); + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error("bus request timed out")); + this._pendingRequests.delete(requestId); + }, timeout); + this._pendingRequests.set(requestId, { resolve, reject, timeout: timeoutId }); + this._sendPayload(message, { needResponse: requestId, batch }); + }); + } + + /** + * @param message any JSON-serializable object + * @param {Object} [options] + * @param {boolean} [options.batch] + */ + send(message, { batch } = {}) { + this._sendPayload(message, { batch }); + } + + /** + * @param {string} event + * @param {Function} func + */ + _onSocket(event, func) { + if (this._isWebsocketEmitter) { + this._websocket.on(event, func); + } else { + this._websocket.addEventListener(event, func); + } + } + + /** + * @param {string} event + * @param {Function} func + */ + _offSocket(event, func) { + if (this._isWebsocketEmitter) { + this._websocket.off(event, func); + } else { + this._websocket.removeEventListener(event, func); + } + } + + _getNextRequestId() { + return `${Bus._type}_${this.id}_${this._requestCount++}`; + } + + /** + * @param message any JSON-serializable object + * @param {Object} [options] + * @param {string} [options.needResponse] if set, the message is a `request` that expects a response with the same id. + * @param {string} [options.responseTo] if set, the message is a response to a request of that id. + * @param {boolean} [options.batch] true if batching the message + */ + _sendPayload(message, { needResponse, responseTo, batch } = {}) { + if (batch) { + this._batch({ message, needResponse, responseTo }); + return; + } + this._websocket.send(JSON.stringify([{ message, needResponse, responseTo }])); + } + + /** + * The delay for gathering the batch happens at the trailing end of the call, meaning that if there is no batch currently + * gathering requests, the first request is sent immediately (to be lenient with infrequent messages), and new batch gathering + * phase starts gathering the subsequent ones. + * + * @param {any} payload any JSON serializable + */ + _batch(payload) { + this._messageQueue.push(payload); + if (this._batchTimeout) { + // the messages will be flushed in currently gathering batch + return; + } + this._flush(); + } + + _flush() { + if (this._messageQueue.length) { + this._websocket.send(JSON.stringify(this._messageQueue)); + this._messageQueue = []; + this._startGathering(); + } + } + + _startGathering() { + this._batchTimeout = setTimeout(() => { + this._flush(); + this._batchTimeout = undefined; + }, this._batchDelay); + } + + /** + * @param webSocketMessage the structure of the webSocketMessage varies depending on whether the websocket is an EventEmitter (node) or an EventTarget (browser) + */ + _onMessage(webSocketMessage) { + const normalizedMessage = this._isWebsocketEmitter + ? webSocketMessage + : webSocketMessage.data; + /** @type {Payload[]} */ + const payloads = JSON.parse(normalizedMessage); + for (const payload of payloads) { + // not awaiting, handled in parallel + this._handlePayload(payload); + } + } + + /** + * Handles incoming messages and dispatches them to the right handler based on whether they are requests, responses or plain messages. + * + * @param {Payload} payload + */ + async _handlePayload({ message, needResponse, responseTo }) { + if (responseTo) { + const pendingRequest = this._pendingRequests.get(responseTo); + clearTimeout(pendingRequest?.timeout); + pendingRequest?.resolve(message); + this._pendingRequests.delete(responseTo); + } else if (needResponse) { + const response = await this.onRequest?.(message); + this._sendPayload(response, { responseTo: needResponse }); + } else { + this.onMessage?.(message); + } + } +} + +// separate file to avoid circular dependencies or increasing bundle size (despite tree shaking) +// it also allows documentation to be shared between client and server + +const WS_CLOSE_CODE = { + CLEAN: 1000, + LEAVING: 1001, + ERROR: 1011, + // 4000-4999 range available for app specific use + AUTHENTICATION_FAILED: 4106, + TIMEOUT: 4107, + KICKED: 4108, + CHANNEL_FULL: 4109, +}; + +const SERVER_REQUEST = { + /** Requests the creation of a consumer that is used to forward a track to the client */ + INIT_CONSUMER: "INIT_CONSUMER", + /** Requests the creation of upload and download transports */ + INIT_TRANSPORTS: "INIT_TRANSPORTS", + /** Requests any response to keep the session alive */ + PING: "PING", +}; + +const SERVER_MESSAGE = { + /** Signals the clients that one of the session in their channel has left. */ + SESSION_LEAVE: "SESSION_LEAVE", + /** Signals the clients that the info (talking, mute,...) of one of the session in their channel has changed. */ + INFO_CHANGE: "S_INFO_CHANGE", +}; + +const CLIENT_REQUEST = { + /** Requests the server to connect the client-to-server transport, this occurs the first time a producer is added to this transport */ + CONNECT_CTS_TRANSPORT: "CONNECT_CTS_TRANSPORT", + /** Requests the server to connect the server-to-client transport, this occurs the first time a consumer is added to this transport */ + CONNECT_STC_TRANSPORT: "CONNECT_STC_TRANSPORT", + /** Requests the creation of a consumer that is used to upload a track to the server */ + INIT_PRODUCER: "INIT_PRODUCER", +}; + +const CLIENT_MESSAGE = { + /** Signals that the client wants to change how it consumes a track (like pausing or ending the download) */ + CONSUMPTION_CHANGE: "CONSUMPTION_CHANGE", + /** Signals that the info (talking, mute,...) of this client has changed. */ + INFO_CHANGE: "C_INFO_CHANGE", + /** Signals that the client wants to change how it produces a track (like pausing or ending the upload) */ + PRODUCTION_CHANGE: "PRODUCTION_CHANGE", +}; + +// eslint-disable-next-line node/no-unpublished-import + +const INITIAL_RECONNECT_DELAY = 1_000; // the initial delay between reconnection attempts +const MAXIMUM_RECONNECT_DELAY = 30_000; // the longest delay possible between reconnection attempts +const MAX_ERRORS = 6; // how many errors should occur before trying a full restart of the connection +const RECOVERY_DELAY = 1_000; // how much time after an error should pass before a soft recovery attempt (retry the operation and not the whole connection) +const SUPPORTED_TYPES = new Set(["audio", "camera", "screen"]); + +// https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions +const PRODUCER_OPTIONS = { + stopTracks: false, + disableTrackOnPause: false, + zeroRtpOnPause: true, +}; + +/** + * @typedef {Object} Consumers + * @property {import("mediasoup-client").types.Consumer | null} audio + * @property {import("mediasoup-client").types.Consumer | null} camera + * @property {import("mediasoup-client").types.Consumer | null} screen + */ + +/** + * @typedef {Object} Producers + * @property {import("mediasoup-client").types.Producer | null} audio + * @property {import("mediasoup-client").types.Producer | null} camera + * @property {import("mediasoup-client").types.Producer | null} screen + */ + +/** + * @typedef {'audio' | 'camera' | 'video' } streamType + */ + +const SFU_CLIENT_STATE = Object.freeze({ + /** + * The client is not connected to the server and does not want to do so. This state is intentional and + * is only set at the creation of a sfuClient, or when the client calls `disconnect`. + */ + DISCONNECTED: "disconnected", + /** + * The client is trying to connect to the server, it is not authenticated yet. + */ + CONNECTING: "connecting", + /** + * The initial handshake with the server has been done and the client is authenticated, the bus is ready to be used. + */ + AUTHENTICATED: "authenticated", + /** + * The client is ready to send and receive tracks. + */ + CONNECTED: "connected", + /** + * This state is reached when the connection is lost and the client is trying to reconnect. + */ + RECOVERING: "recovering", + /** + * This state is reached when the connection is stopped and there should be no automated attempt to reconnect. + */ + CLOSED: "closed", +}); + +const ACTIVE_STATES = new Set([ + SFU_CLIENT_STATE.CONNECTING, + SFU_CLIENT_STATE.AUTHENTICATED, + SFU_CLIENT_STATE.CONNECTED, +]); + +/** + * This class is run by the client and represents the server and abstracts the mediasoup API away. + * It handles authentication, connection recovery and transport/consumers/producers maintenance. + * + * @fires SfuClient#stateChange + * @fires SfuClient#update + */ +class SfuClient extends EventTarget { + /** @type {Error[]} */ + errors = []; + /** @type {SFU_CLIENT_STATE[keyof SFU_CLIENT_STATE]} */ + _state = SFU_CLIENT_STATE.DISCONNECTED; + /** @type {Bus | undefined} */ + _bus; + /** @type {string} */ + _jsonWebToken; + /** @type {import("mediasoup-client").types.Device} */ + _device; + _recoverProducerTimeouts = { + /** @type {number} */ + audio: undefined, + /** @type {number} */ + camera: undefined, + /** @type {number} */ + screen: undefined, + }; + /** @type {import("mediasoup-client").types.Transport} Client-To-Server Transport */ + _ctsTransport; + /** @type {import("mediasoup-client").types.Transport} Server-To-Client Transport */ + _stcTransport; + /** @type {number} */ + _connectRetryDelay = INITIAL_RECONNECT_DELAY; + /** @type {WebSocket} */ + _webSocket; + /** @type {Map} */ + _consumers = new Map(); + /** @type {Producers} */ + _producers = { + audio: null, + camera: null, + screen: null, + }; + /** @type {Function[]} */ + _cleanups = []; + + constructor() { + super(); + this._handleMessage = this._handleMessage.bind(this); + this._handleRequest = this._handleRequest.bind(this); + this._handleConnectionEnd = this._handleConnectionEnd.bind(this); + } + + /** + * @param {SFU_CLIENT_STATE[keyof SFU_CLIENT_STATE]} state + * @fires SfuClient#stateChange + */ + set state(state) { + this._state = state; + /** + * @event SfuClient#stateChange + * @type {Object} + * @property {Object} detail + * @property {string} detail.state + */ + this.dispatchEvent(new CustomEvent("stateChange", { detail: { state } })); + } + + /** + * @returns {SFU_CLIENT_STATE[keyof SFU_CLIENT_STATE]} + */ + get state() { + return this._state; + } + + /** + * @param {string} url + * @param {string} jsonWebToken + * @param {Object} [options] + * @param {[]} [options.iceServers] + */ + async connect(url, jsonWebToken, { iceServers } = {}) { + // saving the options for so that the parameters are saved for reconnection attempts + this._url = url.replace(/^http/, "ws"); // makes sure the url is a websocket url + this._jsonWebToken = jsonWebToken; + this._iceServers = iceServers; + this._connectRetryDelay = INITIAL_RECONNECT_DELAY; + this._device = this._createDevice(); + await this._connect(); + } + + disconnect() { + this._clear(); + this.state = SFU_CLIENT_STATE.DISCONNECTED; + } + + /** + * @returns {Promise<{ uploadStats: RTCStatsReport, downloadStats: RTCStatsReport }>} + */ + async getStats() { + const stats = {}; + const [uploadStats, downloadStats] = await Promise.all([ + this._ctsTransport?.getStats(), + this._stcTransport?.getStats(), + ]); + stats.uploadStats = uploadStats; + stats.downloadStats = downloadStats; + const proms = []; + for (const [type, producer] of Object.entries(this._producers)) { + if (producer) { + proms.push( + (async () => { + stats[type] = await producer.getStats(); + })() + ); + } + } + await Promise.all(proms); + return stats; + } + + /** + * Updates the server with the info of the session (isTalking, isCameraOn,...) so that it can broadcast it to the + * other call participants. + * + * @param {import("#src/models/session.js").SessionInfo} info + * @param {Object} [param0] + * @param {boolean} [param0.needRefresh] true if the server should refresh the local info from all sessions of this channel + */ + updateInfo(info, { needRefresh } = {}) { + this._info = info; + this._bus.send( + { + name: CLIENT_MESSAGE.INFO_CHANGE, + payload: { info, needRefresh }, + }, + { batch: true } + ); + } + + /** + * Stop or resume the consumption of tracks from the other call participants. + * + * @param {number} sessionId + * @param {Object<[streamType, boolean]>} states e.g: { audio: true, camera: false } + */ + updateDownload(sessionId, states) { + const consumers = this._consumers.get(sessionId); + if (!consumers) { + return; + } + let hasChanged = false; + for (const [type, active] of Object.entries(states)) { + if (!SUPPORTED_TYPES.has(type)) { + continue; + } + const consumer = consumers[type]; + if (consumer) { + if (active !== consumer.paused) { + continue; + } + hasChanged = true; + if (active) { + consumer.resume(); + } else { + consumer.pause(); + } + } + } + if (!hasChanged) { + return; + } + this._bus.send( + { + name: CLIENT_MESSAGE.CONSUMPTION_CHANGE, + payload: { sessionId, states }, + }, + { batch: true } + ); + } + + /** + * @param {streamType} type + * @param {MediaStreamTrack | null} track track to be sent to the other call participants, + * not setting it will remove the track from the server + */ + async updateUpload(type, track) { + if (!SUPPORTED_TYPES.has(type)) { + throw new Error(`Unsupported media type ${type}`); + } + clearTimeout(this._recoverProducerTimeouts[type]); + const existingProducer = this._producers[type]; + if (existingProducer) { + if (track) { + await existingProducer.replaceTrack({ track }); + } + this._bus.send( + { + name: CLIENT_MESSAGE.PRODUCTION_CHANGE, + payload: { type, active: Boolean(track) }, + }, + { batch: true } + ); + return; + } + if (!track) { + return; + } + try { + this._producers[type] = await this._ctsTransport.produce({ + ...PRODUCER_OPTIONS, + track, + appData: { type }, + }); + } catch (error) { + this.errors.push(error); + // if we reach the max error count, we restart the whole connection from scratch + if (this.errors.length > MAX_ERRORS) { + // not awaited + this._handleConnectionEnd(); + return; + } + // retry after some delay + this._recoverProducerTimeouts[type] = setTimeout(async () => { + await this.updateUpload(type, track); + }, RECOVERY_DELAY); + return; + } + this._onCleanup(() => { + this._producers[type]?.close(); + this._producers[type] = null; + clearTimeout(this._recoverProducerTimeouts[type]); + }); + } + + /** + * To be overridden in tests. + * + * @returns {import("mediasoup-client").types.Device} + */ + _createDevice() { + return new lib$2.Device(); + } + + /** + * To be overridden in tests. + * + * @param {string} url + * @returns {WebSocket} + * @private + */ + _createWebSocket(url) { + return new WebSocket(url); + } + + /** + * Opens the webSocket connection to the server and authenticates, handles reconnection attempts. + */ + async _connect() { + if (ACTIVE_STATES.has(this.state)) { + return; + } + this._clear(); + this.state = SFU_CLIENT_STATE.CONNECTING; + try { + this._bus = await this._createBus(); + this.state = SFU_CLIENT_STATE.AUTHENTICATED; + } catch { + this._handleConnectionEnd(); + return; + } + this._bus.onMessage = this._handleMessage; + this._bus.onRequest = this._handleRequest; + } + + /** + * @param {string} [cause] + * @private + */ + _close(cause) { + this._clear(); + const state = SFU_CLIENT_STATE.CLOSED; + this._state = state; + this.dispatchEvent(new CustomEvent("stateChange", { detail: { state, cause } })); + } + + /** + * @returns {Promise} + */ + _createBus() { + return new Promise((resolve, reject) => { + let webSocket; + try { + webSocket = this._createWebSocket(this._url); + } catch (error) { + reject(error); + return; + } + webSocket.addEventListener("close", this._handleConnectionEnd); + webSocket.addEventListener("error", this._handleConnectionEnd); + this._onCleanup(() => { + webSocket.removeEventListener("close", this._handleConnectionEnd); + webSocket.removeEventListener("error", this._handleConnectionEnd); + if (webSocket.readyState < webSocket.CLOSING) { + webSocket.close(WS_CLOSE_CODE.CLEAN); + } + }); + /** + * Websocket handshake with the rtc server, + * when opening the webSocket, the server expects the first message to contain the jwt. + */ + webSocket.addEventListener( + "open", + () => { + webSocket.send(JSON.stringify(this._jsonWebToken)); + }, + { once: true } + ); + /** + * Receiving a message means that the server has authenticated the client and is ready to receive messages. + */ + webSocket.addEventListener( + "message", + () => { + resolve(new Bus(webSocket)); + }, + { once: true } + ); + }); + } + + /** + * @param {Function} callback + */ + _onCleanup(callback) { + this._cleanups.push(callback); + } + + _clear() { + for (const cleanup of this._cleanups.splice(0)) { + cleanup(); + } + this.errors = []; + for (const consumers of this._consumers.values()) { + for (const consumer of Object.values(consumers)) { + consumer?.close(); + } + } + this._consumers.clear(); + } + + /** + * @param {import("#src/models/session.js").SessionInfo.TransportConfig} ctsConfig + */ + _makeCTSTransport(ctsConfig) { + const transport = this._device.createSendTransport({ + ...ctsConfig, + iceServers: this._iceServers, + }); + transport.on("connect", async ({ dtlsParameters }, callback, errback) => { + try { + await this._bus.request({ + name: CLIENT_REQUEST.CONNECT_CTS_TRANSPORT, + payload: { dtlsParameters }, + }); + callback(); + } catch (error) { + errback(error); + } + }); + transport.on("produce", async ({ kind, rtpParameters, appData }, callback, errback) => { + try { + const result = await this._bus.request({ + name: CLIENT_REQUEST.INIT_PRODUCER, + payload: { type: appData.type, kind, rtpParameters }, + }); + callback({ id: result.id }); + } catch (error) { + errback(error); + } + }); + this._ctsTransport = transport; + this._onCleanup(() => transport.close()); + } + + /** + * @param {import("#src/models/session.js").SessionInfo.TransportConfig} stcConfig + */ + _makeSTCTransport(stcConfig) { + const transport = this._device.createRecvTransport({ + ...stcConfig, + iceServers: this._iceServers, + }); + transport.on("connect", async ({ dtlsParameters }, callback, errback) => { + try { + await this._bus.request({ + name: CLIENT_REQUEST.CONNECT_STC_TRANSPORT, + payload: { dtlsParameters }, + }); + callback(); + } catch (error) { + errback(error); + } + }); + this._stcTransport = transport; + this._onCleanup(() => transport.close()); + } + + /** + * @param {number} sessionId + */ + _removeConsumers(sessionId) { + const consumers = this._consumers.get(sessionId); + if (!consumers) { + return; + } + for (const consumer of Object.values(consumers)) { + consumer?.close(); + } + this._consumers.delete(sessionId); + } + + /** + * dispatches an event, intended for the client + * + * @param { "disconnect" | "info_change" | "track" | "error"} name + * @param [payload] + * @fires SfuClient#update + */ + _updateClient(name, payload) { + /** + * @event SfuClient#update + * @type {Object} + * @property {Object} detail + * @property {string} detail.name + * @property {Object} detail.payload + */ + this.dispatchEvent(new CustomEvent("update", { detail: { name, payload } })); + } + + /** + * Handles cases where the connection to the server ends or in error and attempts to recover it if appropriate. + * + * @param {Event | CloseEvent} [event] + */ + _handleConnectionEnd(event) { + if (this.state === SFU_CLIENT_STATE.DISCONNECTED) { + // state DISCONNECTED is intentional, so there is no reason to retry + return; + } + switch (event?.code) { + case WS_CLOSE_CODE.CHANNEL_FULL: + this._close("full"); + return; + case WS_CLOSE_CODE.AUTHENTICATION_FAILED: + case WS_CLOSE_CODE.KICKED: + this._close(); + return; + } + this.state = SFU_CLIENT_STATE.RECOVERING; + // Retry connecting with an exponential backoff. + this._connectRetryDelay = + Math.min(this._connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) + 1000 * Math.random(); + const timeout = setTimeout(this._connect.bind(this), this._connectRetryDelay); + this._onCleanup(() => clearTimeout(timeout)); + } + + /** + * @param {Object} param0 + * @param {string} param0.name + * @param {Object} param0.payload + */ + async _handleMessage({ name, payload }) { + switch (name) { + case SERVER_MESSAGE.SESSION_LEAVE: + { + const { sessionId } = payload; + this._removeConsumers(sessionId); + this._updateClient("disconnect", payload); + } + break; + case SERVER_MESSAGE.INFO_CHANGE: + this._updateClient("info_change", payload); + break; + } + } + + /** + * @param {Object} param0 + * @param {string} param0.name + * @param {Object} [param0.payload] + * @returns {Promise} response to the request, JSON-serializable + */ + async _handleRequest({ name, payload }) { + switch (name) { + case SERVER_REQUEST.INIT_CONSUMER: { + const { id, kind, producerId, rtpParameters, sessionId, type, active } = payload; + let consumers; + if (!this._consumers.has(sessionId)) { + consumers = { + audio: null, + camera: null, + screen: null, + }; + this._consumers.set(sessionId, consumers); + } else { + consumers = this._consumers.get(sessionId); + consumers[type]?.close(); + } + const consumer = await this._stcTransport.consume({ + id, + producerId, + kind, + rtpParameters, + }); + if (!active) { + consumer.pause(); + } else { + consumer.resume(); + } + this._updateClient("track", { type, sessionId, track: consumer.track, active }); + consumers[type] = consumer; + return; + } + case SERVER_REQUEST.INIT_TRANSPORTS: { + const { capabilities, stcConfig, ctsConfig } = payload; + if (!this._device.loaded) { + await this._device.load({ routerRtpCapabilities: capabilities }); + } + this._makeSTCTransport(stcConfig); + this._makeCTSTransport(ctsConfig); + this.state = SFU_CLIENT_STATE.CONNECTED; + return this._device.rtpCapabilities; + } + case SERVER_REQUEST.PING: + return; // the server just needs a response, merely returning is enough + } + } +} + +export { SFU_CLIENT_STATE, SfuClient }; + + +export const __info__ = { + date: '2023-12-08T13:58:38.835Z', + hash: '81ee431', + url: 'https://github.com/odoo/sfu', +}; diff --git a/addons/mail/static/lib/odoo_sfu/odoo_sfu.licenses.txt b/addons/mail/static/lib/odoo_sfu/odoo_sfu.licenses.txt new file mode 100644 index 00000000000..0396099d165 --- /dev/null +++ b/addons/mail/static/lib/odoo_sfu/odoo_sfu.licenses.txt @@ -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 (https://inakibaz.me) + José Luis Millán (https://github.com/jmillan) +License Copyright: +=== + +ISC License + +Copyright © 2015, Iñaki Baz Castillo + +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 +Contributors: + TJ Holowaychuk + Nathan Rajlich (http://n8.io) + Andrew Rhyne +License Copyright: +=== + +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +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 (http://faisalman.com) +Contributors: + Aamir Poonawalla + Admas + algenon + Alvin Portillo + Amumu + Andrea Vaghi + Anton Zhiyanov + Arturo Mejia + Arun Rama Reddy + Austin Pray + Bendeguz + Benjamin Bertrand + Benjamin Urban + boneyao + Carl C Von Lewin + CESAR RAMOS + Chad Killingsworth + Christopher De Cairos + Cyrille David + Dario Vladovic + David Annez + Davit Barbakadze + ddivernois + Deliaz + Demis Palma + dhoko + dianhe + dineshks1 + Dmitry Tyschenko + Douglas Li + Dumitru Uzun + Dustin + Elchin Valiyev + Emil Hesslow + Eric Redon + Eric Schrenker + Erik Hesselink + Fabian Becker + Faisal Salman + Frédéric Camblor + Frederik Ring + Gerald Host + Germán M. Bravo + Grigory Dmitrenko + gulpin + Hendrik Helwich + Hermann Ebert + hr6r + Igor Topal + Ildar Kamalov + insanehong + jackpoll + Jake Mc + JBYoshi <12983479+JBYoshi@users.noreply.github.com> + Joey Parrish + John Tantalo + John Yanarella + Jon Buckley + Josh Goldberg + Junki-Ishida + Kendall Buchanan + Lee Treveil + leonardo + Levente Balogh + Liam Quinn + Lithin + ll-syber <670159357@qq.com> + Loris Guignard + Lukas Drgon + Lukas Eipert + Malash + Martynas + Masahiko Sato + Matt Brophy + Matthew Origer + Maximilian Haupt + Max Maurer + Max Nordlund + Michael Hess + MimyyK + naoh + Nicholas Ionata + Nikhil Motiani + Nik Rolls + nionata + niris + Nobuo Okada + o.drapeza + otakuSiD + patrick-nurt + Pavel Studeny + Peter Dave Hello + philippsimon + Pieter Hendrickx + Piper Chester + Queen Vinyl Darkscratch + Raine Makelainen + Raman Savaryn + Robert Tod + roman.savarin + Ron Korland + Ross Noble + ruicong <466403866@qq.com> + Sandro Sonntag + sgautrea + Shane Gautreau + Shane Thacker + Shreedhar + Simon Eisenmann + Simon Lang + Stiekel + sUP + Sylvain Gizard + szchenghuang + Tanguy Krotoff + Tony Tomarchio + Ulrich Schmidt + Vadim Kurachevsky + Yılmaz + yuanyang + Yun Young-jin + Zach Bjornson +License Copyright: +=== + +MIT License + +Copyright (c) 2012-2023 Faisal Salman <> + +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 (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 (https://inakibaz.me) +License Copyright: +=== + +ISC License + +Copyright © 2019, Iñaki Baz Castillo + +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 (https://inakibaz.me) +License Copyright: +=== + +ISC License + +Copyright © 2019, Iñaki Baz Castillo + +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 (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 +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. \ No newline at end of file diff --git a/addons/mail/static/src/discuss/call/common/call_context_menu.js b/addons/mail/static/src/discuss/call/common/call_context_menu.js index 4163cf3b5ac..cdbafaf5a58 100644 --- a/addons/mail/static/src/discuss/call/common/call_context_menu.js +++ b/addons/mail/static/src/discuss/call/common/call_context_menu.js @@ -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({ diff --git a/addons/mail/static/src/discuss/call/common/call_context_menu.xml b/addons/mail/static/src/discuss/call/common/call_context_menu.xml index b0e029a0062..f338e3fd606 100644 --- a/addons/mail/static/src/discuss/call/common/call_context_menu.xml +++ b/addons/mail/static/src/discuss/call/common/call_context_menu.xml @@ -2,22 +2,56 @@
- - + +
-
Connection:
-
ICE:
-
DTLS:
-
Data channel:
-
Audio player:
-
Video player:
+
Connection type:
+
To peer:
+
From peer:
+
Connection:
+
ICE:
+
DTLS:
+
Data channel:
+
Audio player:
+
Video player:

-
ICE gathering:
-
From peer:
-
To peer:
-
Packets sent:
-
Packets received:
-
Log step:
+
ICE gathering:
+
Packets sent:
+
Packets received:
+
Log step:
+
+ +
Connection type:
+
RTC Session ID:
+
+
Upload:
+
up ICE:
+
up DTLS:
+
Packets sent:
+
available bitrate:
+
+
Download:
+
down ICE:
+
down DTLS:
+
Packets received:
+ +
+
microphone
+
codec:
+
clock rate:
+
+ +
+
camera
+
codec:
+
clock rate:
+
+ +
+
screen
+
codec:
+
clock rate:
+
diff --git a/addons/mail/static/src/discuss/call/common/call_participant_card.js b/addons/mail/static/src/discuss/call/common/call_participant_card.js index 6368090f80e..3115d496e51 100644 --- a/addons/mail/static/src/discuss/call/common/call_participant_card.js +++ b/addons/mail/static/src/discuss/call/common/call_participant_card.js @@ -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; } diff --git a/addons/mail/static/src/discuss/call/common/call_participant_card.xml b/addons/mail/static/src/discuss/call/common/call_participant_card.xml index 53e19266810..aade64e5b68 100644 --- a/addons/mail/static/src/discuss/call/common/call_participant_card.xml +++ b/addons/mail/static/src/discuss/call/common/call_participant_card.xml @@ -56,6 +56,9 @@ + + + LIVE diff --git a/addons/mail/static/src/discuss/call/common/media_monitoring.js b/addons/mail/static/src/discuss/call/common/media_monitoring.js index cad6b8bed48..c814a909305 100644 --- a/addons/mail/static/src/discuss/call/common/media_monitoring.js +++ b/addons/mail/static/src/discuss/call/common/media_monitoring.js @@ -73,7 +73,7 @@ function _loadScriptProcessor( audioContext, { frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE, - minimumActiveCycles = 10, + minimumActiveCycles = 30, onThreshold, onTic, volumeThreshold = 0.3, diff --git a/addons/mail/static/src/discuss/call/common/rtc_service.js b/addons/mail/static/src/discuss/call/common/rtc_service.js index 9cbda23e5fa..3196c3cd627 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_service.js +++ b/addons/mail/static/src/discuss/call/common/rtc_service.js @@ -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} timeoutId by notificationId for call notifications */ timeouts = new Map(); + /** @type {Map} 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 }) => { diff --git a/addons/mail/static/src/discuss/call/common/rtc_session_model.js b/addons/mail/static/src/discuss/call/common/rtc_session_model.js index cfd6f0754fe..5c076c6f26d 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_session_model.js +++ b/addons/mail/static/src/discuss/call/common/rtc_session_model.js @@ -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; diff --git a/addons/mail/static/src/worklets/audio_processor.js b/addons/mail/static/src/worklets/audio_processor.js index 060b6cfba55..2f559369a72 100644 --- a/addons/mail/static/src/worklets/audio_processor.js +++ b/addons/mail/static/src/worklets/audio_processor.js @@ -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, }, diff --git a/addons/mail/tests/discuss/test_rtc.py b/addons/mail/tests/discuss/test_rtc.py index a04c60f8e4e..8d5802dfb7d 100644 --- a/addons/mail/tests/discuss/test_rtc.py +++ b/addons/mail/tests/discuss/test_rtc.py @@ -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') diff --git a/addons/mail/tools/discuss.py b/addons/mail/tools/discuss.py index 29c4274ba61..5ef8de9a26a 100644 --- a/addons/mail/tools/discuss.py +++ b/addons/mail/tools/discuss.py @@ -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') diff --git a/addons/mail/tools/jwt.py b/addons/mail/tools/jwt.py new file mode 100644 index 00000000000..066e2529248 --- /dev/null +++ b/addons/mail/tools/jwt.py @@ -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) diff --git a/addons/mail/views/discuss_channel_views.xml b/addons/mail/views/discuss_channel_views.xml index 9faaf52dcd8..20263e0ffd8 100644 --- a/addons/mail/views/discuss_channel_views.xml +++ b/addons/mail/views/discuss_channel_views.xml @@ -66,6 +66,10 @@ + + + + diff --git a/addons/mail/views/res_config_settings_views.xml b/addons/mail/views/res_config_settings_views.xml index 8db6055d2dc..829fd66e0c9 100644 --- a/addons/mail/views/res_config_settings_views.xml +++ b/addons/mail/views/res_config_settings_views.xml @@ -75,6 +75,18 @@ + +
+
+
+
+
+
+
diff --git a/addons/mail/web_push.py b/addons/mail/web_push.py index 417b99f0196..956f7d19f3e 100644 --- a/addons/mail/web_push.py +++ b/addons/mail/web_push.py @@ -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', }