├── devRequirements.txt ├── nbwhisper ├── v1 │ ├── __init__.py │ └── handlers.py ├── _version.py ├── __init__.py ├── nbextension │ ├── resources │ │ ├── back.svg │ │ ├── close.svg │ │ ├── plus.svg │ │ ├── hamburger.svg │ │ ├── check_off.svg │ │ ├── check_on.svg │ │ ├── stretch_screen.svg │ │ ├── minimize_screen.svg │ │ ├── mic_on_palette.svg │ │ ├── mic_on.svg │ │ ├── maximize_screen.svg │ │ ├── close_talk.svg │ │ ├── hang_on.svg │ │ ├── mic_off_palette.svg │ │ ├── maximize.svg │ │ ├── mic_off.svg │ │ ├── mic_off_list.svg │ │ ├── share_display_palette.svg │ │ ├── share_display.svg │ │ ├── members.svg │ │ ├── share_display_list.svg │ │ ├── hang_up_palette.svg │ │ ├── hang_up.svg │ │ └── on_talking.svg │ ├── readme.md │ ├── notebook.yaml │ ├── main.css │ └── main.js ├── server.py └── config.py ├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── images └── configurator.png ├── example └── jupyter_notebook_config.py ├── Dockerfile ├── setup.py ├── LICENSE.txt └── README.md /devRequirements.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /nbwhisper/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | notebook>=6.5.4 2 | tornado 3 | traitlets -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include nbwhisper/nbextension/* 2 | include nbwhisper/nbextension/resources/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .python-version 2 | nbwhisper_config.py 3 | **/*.pyc 4 | __pycache__ 5 | .DS_Store 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /images/configurator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NII-cloud-operation/nbwhisper/main/images/configurator.png -------------------------------------------------------------------------------- /nbwhisper/_version.py: -------------------------------------------------------------------------------- 1 | version_info = (0, 1, 0, 'dev1') 2 | __version__ = '.'.join(map(str, version_info)) 3 | -------------------------------------------------------------------------------- /example/jupyter_notebook_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | c.NBWhisper.skyway_api_token = os.environ.get('NBWHISPER_SKYWAY_API_TOKEN', '') 4 | c.NBWhisper.room_mode_for_waiting_room = os.environ.get('NBWHISPER_ROOM_MODE_FOR_WAITING_ROOM', 'sfu') 5 | c.NBWhisper.room_mode_for_talking_room = os.environ.get('NBWHISPER_ROOM_MODE_FOR_TALKING_ROOM', 'mesh') 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM niicloudoperation/notebook:latest 2 | 3 | USER root 4 | 5 | COPY . /tmp/nbwhisper 6 | RUN pip install /tmp/nbwhisper && \ 7 | jupyter nbclassic-extension install --py nbwhisper --sys-prefix && \ 8 | jupyter nbclassic-serverextension enable --py nbwhisper --sys-prefix && \ 9 | jupyter nbclassic-extension enable --py nbwhisper --sys-prefix 10 | 11 | # Configuration for Server Proxy 12 | RUN cat /tmp/nbwhisper/example/jupyter_notebook_config.py >> $CONDA_DIR/etc/jupyter/jupyter_notebook_config.py 13 | 14 | USER $NB_UID 15 | -------------------------------------------------------------------------------- /nbwhisper/__init__.py: -------------------------------------------------------------------------------- 1 | """The NBWhisper Server""" 2 | 3 | import os 4 | from . import ( 5 | server, 6 | ) 7 | 8 | 9 | def load_jupyter_server_extension(nb_server_app): 10 | nb_server_app.log.info('nbsearch extension started') 11 | server.register_routes(nb_server_app, nb_server_app.web_app) 12 | 13 | 14 | # nbextension 15 | def _jupyter_nbextension_paths(): 16 | notebook_ext = dict(section='notebook', 17 | src='nbextension', 18 | dest='nbwhisper', 19 | require='nbwhisper/main') 20 | return [notebook_ext] 21 | 22 | 23 | # server extension 24 | def _jupyter_server_extension_paths(): 25 | return [dict(module='nbwhisper')] 26 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_close 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /nbwhisper/v1/handlers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import os 3 | from stat import S_IREAD 4 | 5 | from tornado import web 6 | from jupyter_server.base.handlers import JupyterHandler 7 | 8 | 9 | class ConfigHandler(JupyterHandler): 10 | def initialize(self, config): 11 | self._config = config 12 | 13 | @web.authenticated 14 | async def get(self): 15 | username = self._config.default_username 16 | if 'JUPYTERHUB_USER' in os.environ: 17 | username = os.environ['JUPYTERHUB_USER'] 18 | self.finish({ 19 | 'username': username, 20 | 'skyway_api_token': self._config.skyway_api_token, 21 | 'room_mode_for_waiting_room': self._config.room_mode_for_waiting_room, 22 | 'room_mode_for_talking_room': self._config.room_mode_for_talking_room, 23 | }) 24 | -------------------------------------------------------------------------------- /nbwhisper/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tornado import gen 4 | 5 | from .v1.handlers import ( 6 | ConfigHandler 7 | ) 8 | from .config import NBWhisper 9 | 10 | 11 | def get_api_handlers(parent_app, base_dir): 12 | config = NBWhisper(parent=parent_app) 13 | handler_settings = {} 14 | handler_settings['config'] = config 15 | 16 | return [ 17 | (r"/v1/config", ConfigHandler, handler_settings), 18 | ] 19 | 20 | def register_routes(nb_server_app, web_app): 21 | from notebook.utils import url_path_join 22 | api_handlers = get_api_handlers(nb_server_app, nb_server_app.notebook_dir) 23 | 24 | host_pattern = '.*$' 25 | handlers = [(url_path_join(web_app.settings['base_url'], 'nbwhisper', path), 26 | handler, 27 | options) 28 | for path, handler, options in api_handlers] 29 | web_app.add_handlers(host_pattern, handlers) 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import os 5 | import shutil 6 | 7 | HERE = os.path.abspath(os.path.dirname(__file__)) 8 | VERSION_NS = {} 9 | with open(os.path.join(HERE, 'nbwhisper', '_version.py')) as f: 10 | exec(f.read(), {}, VERSION_NS) 11 | 12 | setup_args = dict(name='nbwhisper', 13 | version=VERSION_NS['__version__'], 14 | description='NBWhisper Jupyter Extension', 15 | author='NII', 16 | packages=find_packages(), 17 | include_package_data=True, 18 | zip_safe=False, 19 | platforms=['Jupyter Notebook 6.x'], 20 | install_requires=[ 21 | 'notebook>=6.5.4', 22 | 'tornado', 23 | 'traitlets', 24 | ], 25 | extras_require={ 26 | 'test': [ 27 | 'pytest', 28 | ], 29 | }, 30 | ) 31 | 32 | if __name__ == '__main__': 33 | setup(**setup_args) -------------------------------------------------------------------------------- /nbwhisper/config.py: -------------------------------------------------------------------------------- 1 | from traitlets import Unicode 2 | from traitlets.config.configurable import Configurable 3 | 4 | 5 | class NBWhisper(Configurable): 6 | 7 | skyway_api_token = Unicode( 8 | '', 9 | help='An api token for SkyWay(*for old SkyWay). You need to use the same api key in your team. Please see SkyWay: https://console-webrtc-free.ecl.ntt.com/users/login', 10 | ).tag(config=True) 11 | 12 | room_mode_for_waiting_room = Unicode( 13 | 'sfu', 14 | help='Room mode for waiting room. You can input "sfu" or "mesh". You need to use the same mode in your team.', 15 | ).tag(config=True) 16 | 17 | room_mode_for_talking_room = Unicode( 18 | 'mesh', 19 | help='Room mode for talking room. You can input "sfu" or "mesh". You need to use the same mode in your team.', 20 | ).tag(config=True) 21 | 22 | default_username = Unicode('jovyan', help='Default Username').tag(config=True) 23 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ↳📍Icon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/readme.md: -------------------------------------------------------------------------------- 1 | ## NBWhisper 2 | This nbextension allows using WebRTC in your blowser. You can talk with your voice and share your blowser tab you are editting by WebRTC. WebRTC services are offered by SkyWay. 3 | ### SkyWay Website 4 | https://skyway.ntt.com/ja/ 5 | ### options 6 | `An api token for SkyWay.`
7 | Input your api token offered by SkyWay. This nbextension is not compatible with new SkyWay service, but old one. You can get an api token from your old SkyWay admin page.
8 | https://console-webrtc-free.ecl.ntt.com/users/login
9 | - You must input a domain of your OperationHub page in available domain names. 10 | - You can connect to only users who have the same api key. You must input the same token with your team members. 11 | 12 | `Room mode for waiting room.`
13 | Input "sfu" or "mesh" as waiting room mode you want to use. 14 | - You must input the same mode with your team members. 15 | 16 | `Room mode for talking room.`
17 | Input "sfu" or "mesh" as talking room mode you want to use. 18 | - You must input the same mode with your team members.
19 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/notebook.yaml: -------------------------------------------------------------------------------- 1 | Type: Jupyter Notebook Extension 2 | Name: NBWhisper 3 | Section: notebook 4 | Description: Adds a feature for having voice chat and sharing screens 5 | Link: readme.md 6 | Main: main.js 7 | # 1.x means nbclassic - leave 6.x in place just in case. 8 | Compatibility: 1.x 6.x 9 | Parameters: 10 | - name: nbwhisper_skyway_api_token 11 | description: An api token for SkyWay(*for old SkyWay). You need to use the same api key in your team. Please see SkyWay. If not specified, the server settings will be applied. 12 | input_type: text 13 | default: '' 14 | - name: nbwhisper_room_mode_for_waiting_room 15 | description: Room mode for waiting room. You can input "sfu" or "mesh". You need to use the same mode in your team. If not specified, the server settings will be applied. 16 | input_type: text 17 | - name: nbwhisper_room_mode_for_talking_room 18 | description: Room mode for talking room. You can input "sfu" or "mesh". You need to use the same mode in your team. If not specified, the server settings will be applied. 19 | input_type: text 20 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, National Institute of Informatics. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of National Institute of Informatics nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ↳📍Icon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NBWhisper 2 | This NB extension allows you to use WebRTC in your browser. You can talk and share your tab's view with colleagues on the same JupyterHub/OperationHub. 3 | 4 | > [!NOTE] 5 | > We will leave SkyWay soon and adopt "WebMeeting Software Suite" (https://meeting.dev/index.html). 6 | > 7 | 8 | Currently, this implementation depends on SkyWay (https://skyway.ntt.com/ja/) 9 | 10 | ### options 11 | 12 | You can specify the following items either from `jupyter_notebook_config.py` or from the Jupyter Nbextensions Configurator. 13 | 14 | `An api token for SkyWay.`
15 | Input your api token offered by SkyWay. This nbextension is not compatible with new SkyWay service, but old one. You can get an api token from your old SkyWay admin page.
16 | https://console-webrtc-free.ecl.ntt.com/users/login
17 | - You must input a domain of your OperationHub page in available domain names. 18 | - You can connect to only users who have the same api key. You must input the same token with your team members. 19 | 20 | `Room mode for waiting room.`
21 | Input "sfu" or "mesh" as waiting room mode you want to use. 22 | - You must input the same mode with your team members. 23 | 24 | `Room mode for talking room.`
25 | Input "sfu" or "mesh" as talking room mode you want to use. 26 | - You must input the same mode with your team members.
27 | 28 | #### Configuration on jupyter_notebook_config.py 29 | 30 | ``` 31 | c.NBWhisper.skyway_api_token = 'An api token for SkyWay' 32 | c.NBWhisper.room_mode_for_waiting_room = 'Room mode for waiting room' 33 | c.NBWhisper.room_mode_for_talking_room = 'Room mode for talking room' 34 | ``` 35 | 36 | #### Configuration on the Jupyter Nbextensions Configurator 37 | 38 | ![Configurator](./images/configurator.png) 39 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/check_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone-slash copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/check_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone-slash copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/stretch_screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_arrowToLeft 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/minimize_screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/mic_on_palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/mic_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/maximize_screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/close_talk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/hang_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/mic_off_palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone copy 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/mic_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/mic_off_list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone-slash copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/share_display_palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone copy 3 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/share_display.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/members.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/share_display_list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone-slash copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/hang_up_palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/hang_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/resources/on_talking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/icon_microphone-slash copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /nbwhisper/nbextension/main.css: -------------------------------------------------------------------------------- 1 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .show-list-button { 2 | position: fixed; 3 | bottom: 32px; 4 | right: 32px; 5 | z-index: 10000; 6 | width: 56px; 7 | height: 56px; 8 | background-color: #005BD5; 9 | background-image: url(./resources/hamburger.svg); 10 | background-size: 34px 20px; 11 | background-repeat: no-repeat; 12 | background-position: center; 13 | border-radius: 50%; 14 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 15 | cursor: pointer; 16 | } 17 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .show-list-button:hover { 18 | background-color: #003780; 19 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6); 20 | transition: background-color .5s, box-shadow .5s; 21 | } 22 | 23 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hide-list-button { 24 | position: fixed; 25 | bottom: 32px; 26 | right: 32px; 27 | z-index: 10000; 28 | width: 56px; 29 | height: 56px; 30 | background-color: #ffffff; 31 | background-image: url(./resources/close.svg); 32 | background-size: 20px 20px; 33 | background-repeat: no-repeat; 34 | background-position: center; 35 | border-radius: 50%; 36 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 37 | cursor: pointer; 38 | } 39 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hide-list-button:hover { 40 | background-color: #d0d0d0; 41 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6); 42 | transition: background-color .5s, box-shadow .5s; 43 | } 44 | 45 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .user-list-dialog { 46 | position: absolute; 47 | right: 32px; 48 | bottom: 96px; 49 | width: 280px; 50 | height: 420px; 51 | background-color: #ffffff; 52 | border-radius: 4px; 53 | border: 1px solid #BABABA; 54 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 55 | z-index: 20000; 56 | } 57 | 58 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-talking-button-disabled { 59 | position: absolute; 60 | left: 32px; 61 | bottom: 16px; 62 | right: 32px; 63 | height: 38px; 64 | background-color: #BABABA; 65 | border-radius: 4px; 66 | cursor: pointer; 67 | } 68 | 69 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-talking-button { 70 | position: absolute; 71 | left: 32px; 72 | bottom: 16px; 73 | right: 32px; 74 | height: 38px; 75 | background-color: #005bd5; 76 | border-radius: 4px; 77 | cursor: pointer; 78 | } 79 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-talking-button:hover { 80 | background-color: #003780; 81 | transition: background-color .5s; 82 | } 83 | 84 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .dialog-list-container { 85 | position: absolute; 86 | left: 12px; 87 | top: 24px; 88 | right: 8px; 89 | bottom: 78px; 90 | overflow-y: auto; 91 | overflow-x: hidden; 92 | white-space: nowrap; 93 | } 94 | 95 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table { 96 | border-collapse: separate; 97 | border-spacing: 12px 0px; 98 | width: 100%; 99 | table-layout: fixed; 100 | } 101 | 102 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-td { 103 | position: relative; 104 | height: 30px; 105 | background-color: #ffffff; 106 | } 107 | 108 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-hr { 109 | border: 0px; 110 | border-top: 2px solid #CCDEF7; 111 | } 112 | 113 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-text { 114 | position: absolute; 115 | left: 0; 116 | right: 32px; 117 | top: 50%; 118 | transform: translate(0, -50%); 119 | font-size: 14px; 120 | color: #000000; 121 | overflow: hidden; 122 | text-overflow: ellipsis; 123 | white-space: nowrap; 124 | } 125 | 126 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-text-disabled { 127 | position: absolute; 128 | left: 0; 129 | right: 32px; 130 | top: 50%; 131 | transform: translate(0, -50%); 132 | font-size: 14px; 133 | color: #bababa; 134 | overflow: hidden; 135 | text-overflow: ellipsis; 136 | white-space: nowrap; 137 | } 138 | 139 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-text-in-room { 140 | position: absolute; 141 | left: 0; 142 | right: 45px; 143 | top: 50%; 144 | transform: translate(0, -50%); 145 | font-size: 14px; 146 | color: #000000; 147 | overflow: hidden; 148 | text-overflow: ellipsis; 149 | white-space: nowrap; 150 | } 151 | 152 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-talking-icon { 153 | position: absolute; 154 | right: 4px; 155 | width: 18px; 156 | height: 16px; 157 | top: 50%; 158 | transform: translate(0, -50%); 159 | background-image: url(./resources/on_talking.svg); 160 | background-size: 100% 100%; 161 | background-repeat: no-repeat; 162 | } 163 | 164 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-mute-icon { 165 | position: absolute; 166 | right: 6px; 167 | width: 14px; 168 | height: 14px; 169 | top: 50%; 170 | transform: translate(0, -50%); 171 | background-image: url(./resources/mic_off_list.svg); 172 | background-size: 100% 100%; 173 | background-repeat: no-repeat; 174 | } 175 | 176 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-share-display-icon { 177 | position: absolute; 178 | right: 5px; 179 | width: 16px; 180 | height: 16px; 181 | top: 50%; 182 | transform: translate(0, -50%); 183 | background-image: url(./resources/share_display_list.svg); 184 | background-size: 100% 100%; 185 | background-repeat: no-repeat; 186 | } 187 | 188 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-checking-icon { 189 | position: absolute; 190 | right: 8px; 191 | width: 10px; 192 | height: 6px; 193 | top: 50%; 194 | transform: translate(0, -50%); 195 | background-image: url(./resources/check_off.svg); 196 | background-size: 100% 100%; 197 | background-repeat: no-repeat; 198 | } 199 | 200 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .users-list-table-checked-icon { 201 | position: absolute; 202 | right: 8px; 203 | width: 10px; 204 | height: 6px; 205 | top: 50%; 206 | transform: translate(0, -50%); 207 | background-image: url(./resources/check_on.svg); 208 | background-size: 100% 100%; 209 | background-repeat: no-repeat; 210 | } 211 | 212 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .button-text-white { 213 | position: absolute; 214 | left: 50%; 215 | top: 50%; 216 | transform: translate(-50%, -50%); 217 | color: #ffffff; 218 | font-size: 14px; 219 | font-weight: bold; 220 | white-space: nowrap; 221 | } 222 | 223 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-left { 224 | position: absolute; 225 | left: 0; 226 | width: 8px; 227 | height: 100%; 228 | cursor: ew-resize; 229 | } 230 | 231 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-top { 232 | position: absolute; 233 | top: 0; 234 | width: 100%; 235 | height: 8px; 236 | cursor: ns-resize; 237 | } 238 | 239 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-right { 240 | position: absolute; 241 | right: 0; 242 | width: 8px; 243 | height: 100%; 244 | cursor: ew-resize; 245 | } 246 | 247 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-bottom { 248 | position: absolute; 249 | bottom: 0; 250 | width: 100%; 251 | height: 8px; 252 | cursor: ns-resize; 253 | } 254 | 255 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-topleft { 256 | position: absolute; 257 | left: 0; 258 | top: 0; 259 | width: 8px; 260 | height: 8px; 261 | cursor: nwse-resize; 262 | } 263 | 264 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-topright { 265 | position: absolute; 266 | right: 0; 267 | top: 0; 268 | width: 8px; 269 | height: 8px; 270 | cursor: nesw-resize; 271 | } 272 | 273 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-bottomright { 274 | position: absolute; 275 | right: 0; 276 | bottom: 0; 277 | width: 8px; 278 | height: 8px; 279 | cursor: nwse-resize; 280 | } 281 | 282 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .resize-dialog-bar-bottomleft { 283 | position: absolute; 284 | left: 0; 285 | bottom: 0; 286 | width: 8px; 287 | height: 8px; 288 | cursor: nesw-resize; 289 | } 290 | 291 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .alert-bg-mask { 292 | position: absolute; 293 | left: 0; 294 | top: 0; 295 | width: 100%; 296 | height: 100%; 297 | background-color: rgba(0, 0, 0, 0.6); 298 | z-index: 100000; 299 | } 300 | 301 | @keyframes fadein { 302 | 0% {opacity: 0;} 303 | 100% {opacity: 1;} 304 | } 305 | 306 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-surface { 307 | position: absolute; 308 | width: 680px; 309 | height: 158px; 310 | left: 50%; 311 | top: 40%; 312 | transform: translate(-50%, -50%); 313 | background-color: #ffffff; 314 | border-radius: 4px; 315 | opacity: 0; 316 | animation-name: fadein; 317 | animation-duration: .5s; 318 | animation-timing-function: ease-out; 319 | animation-fill-mode: forwards; 320 | } 321 | 322 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-text { 323 | position: absolute; 324 | left: 56px; 325 | top: 40px; 326 | font-size: 16px; 327 | color: #000000; 328 | } 329 | 330 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-cancel-button { 331 | position: absolute; 332 | left: 56px; 333 | bottom: 38px; 334 | width: 164px; 335 | height: 38px; 336 | background-color: #ffffff; 337 | border-radius: 4px; 338 | border: 2px solid #bababa; 339 | cursor: pointer; 340 | } 341 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-cancel-button:hover { 342 | background-color: #d0d0d0; 343 | transition: background-color .5s; 344 | } 345 | 346 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-ok-button { 347 | position: absolute; 348 | left: 244px; 349 | bottom: 38px; 350 | width: 164px; 351 | height: 38px; 352 | background-color: #005BD5; 353 | border-radius: 4px; 354 | cursor: pointer; 355 | } 356 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-start-talking-alert-ok-button:hover { 357 | background-color: #003780; 358 | transition: background-color .5s; 359 | } 360 | 361 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .button-text-dark-gray { 362 | position: absolute; 363 | left: 50%; 364 | top: 50%; 365 | top: 50%; 366 | transform: translate(-50%, -50%); 367 | color: #5d5d5d; 368 | font-size: 14px; 369 | font-weight: bold; 370 | white-space: nowrap; 371 | } 372 | 373 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .talk-screen-bg-mask { 374 | position: absolute; 375 | left: 100%; 376 | top: 100%; 377 | width: 0; 378 | height: 0; 379 | background-color: rgba(0, 0, 0, 1); 380 | z-index: 30000; 381 | } 382 | 383 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .close-talk-button { 384 | position: absolute; 385 | left: 32px; 386 | bottom: 24px; 387 | width: 56px; 388 | height: 56px; 389 | background-color: #333333; 390 | background-image: url(./resources/close_talk.svg); 391 | background-size: 20px 16px; 392 | background-repeat: no-repeat; 393 | background-position: center; 394 | border-radius: 50%; 395 | cursor: pointer; 396 | } 397 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .close-talk-button:hover { 398 | background-color: #222222; 399 | transition: background-color .5s; 400 | } 401 | 402 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-off-button { 403 | position: absolute; 404 | left: 50%; 405 | bottom: 24px; 406 | width: 56px; 407 | height: 56px; 408 | transform: translate(-50%, 0) translate(-80px, 0); 409 | background-color: #333333; 410 | background-image: url(./resources/mic_on.svg); 411 | background-size: 16px 20px; 412 | background-repeat: no-repeat; 413 | background-position: center; 414 | border-radius: 50%; 415 | cursor: pointer; 416 | } 417 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-off-button:hover { 418 | background-color: #222222; 419 | transition: background-color .5s; 420 | } 421 | 422 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-on-button { 423 | position: absolute; 424 | left: 50%; 425 | bottom: 24px; 426 | width: 56px; 427 | height: 56px; 428 | transform: translate(-50%, 0) translate(-80px, 0); 429 | background-color: #ff2747; 430 | background-image: url(./resources/mic_off.svg); 431 | background-size: 20px 20px; 432 | background-repeat: no-repeat; 433 | background-position: center; 434 | border-radius: 50%; 435 | cursor: pointer; 436 | } 437 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-on-button:hover { 438 | background-color: #bc0001; 439 | transition: background-color .5s; 440 | } 441 | 442 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .share-display-button { 443 | position: absolute; 444 | left: 50%; 445 | bottom: 24px; 446 | width: 56px; 447 | height: 56px; 448 | transform: translate(-50%, 0); 449 | background-color: #333333; 450 | background-image: url(./resources/share_display.svg); 451 | background-size: 22px 22px; 452 | background-repeat: no-repeat; 453 | background-position: center; 454 | border-radius: 50%; 455 | cursor: pointer; 456 | } 457 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .share-display-button:hover { 458 | background-color: #222222; 459 | transition: background-color .5s; 460 | } 461 | 462 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .stop-share-display-button { 463 | position: absolute; 464 | left: 50%; 465 | bottom: 24px; 466 | width: 56px; 467 | height: 56px; 468 | transform: translate(-50%, 0); 469 | background-color: #005BD5; 470 | background-image: url(./resources/share_display.svg); 471 | background-size: 22px 22px; 472 | background-repeat: no-repeat; 473 | background-position: center; 474 | border-radius: 50%; 475 | cursor: pointer; 476 | } 477 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .stop-share-display-button:hover { 478 | background-color: #003780; 479 | transition: background-color .5s; 480 | } 481 | 482 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hang-up-button { 483 | position: absolute; 484 | left: 50%; 485 | bottom: 24px; 486 | width: 56px; 487 | height: 56px; 488 | transform: translate(-50%, 0) translate(80px, 0); 489 | background-color: #ff2747; 490 | background-image: url(./resources/hang_up.svg); 491 | background-size: 21px 21px; 492 | background-repeat: no-repeat; 493 | background-position: center; 494 | border-radius: 50%; 495 | cursor: pointer; 496 | } 497 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hang-up-button:hover { 498 | background-color: #bc0001; 499 | transition: background-color .5s; 500 | } 501 | 502 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .show-members-button { 503 | position: absolute; 504 | right: 32px; 505 | bottom: 24px; 506 | width: 56px; 507 | height: 56px; 508 | background-color: #333333; 509 | background-image: url(./resources/members.svg); 510 | background-size: 22px 22px; 511 | background-repeat: no-repeat; 512 | background-position: center; 513 | border-radius: 50%; 514 | cursor: pointer; 515 | } 516 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .show-members-button:hover { 517 | background-color: #222222; 518 | transition: background-color .5s; 519 | } 520 | 521 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hide-members-button { 522 | position: absolute; 523 | right: 32px; 524 | bottom: 24px; 525 | width: 56px; 526 | height: 56px; 527 | background-color: #005BD5; 528 | background-image: url(./resources/members.svg); 529 | background-size: 22px 22px; 530 | background-repeat: no-repeat; 531 | background-position: center; 532 | border-radius: 50%; 533 | cursor: pointer; 534 | } 535 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hide-members-button:hover { 536 | background-color: #003780; 537 | transition: background-color .5s; 538 | } 539 | 540 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .sharing-user-name-text-container { 541 | position: absolute; 542 | left: 38px; 543 | bottom: 104px; 544 | width: 128px; 545 | height: 30px; 546 | background-color: #333333; 547 | border-radius: 4px; 548 | } 549 | 550 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .sharing-user-name-text { 551 | position: absolute; 552 | left: 0; 553 | top: 50%; 554 | transform: translate(0, -50%); 555 | color: #ffffff; 556 | font-size: 14px; 557 | text-overflow: ellipsis; 558 | white-space: nowrap; 559 | width: 100%; 560 | text-align: center; 561 | } 562 | 563 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .view-container-talk-screen { 564 | position: absolute; 565 | top: 32px; 566 | right: 32px; 567 | bottom: 96px; 568 | left: 32px; 569 | width: calc(100% - 32px - 32px); 570 | height: calc(100% - 32px - 96px); 571 | display: table; 572 | } 573 | 574 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-videos-area-outer { 575 | height: 100%; 576 | display: table-cell; 577 | overflow: hidden; 578 | } 579 | 580 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-videos-area { 581 | position: relative; 582 | width: 100%; 583 | height: 100%; 584 | } 585 | 586 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-videos-area-child { 587 | position: absolute; 588 | width: 100%; 589 | height: 100%; 590 | } 591 | 592 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-video-hidden { 593 | position: absolute; 594 | bottom: 0; 595 | left: 0; 596 | width: 0; 597 | height: 0; 598 | transform: translate(0, 100%); 599 | } 600 | 601 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-video-shown { 602 | position: absolute; 603 | top: 50%; 604 | left: 50%; 605 | transform: translate(-50%, -50%); 606 | max-width: 100%; 607 | max-height: 100%; 608 | } 609 | 610 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-list-view-talk-screen { 611 | position: relative; 612 | width: 0; 613 | height: 100%; 614 | display: table-cell; 615 | } 616 | 617 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-list-view-talk-screen-inner { 618 | position: absolute; 619 | left: 8px; 620 | width: calc(100% - 8px); 621 | height: 100%; 622 | background-color: #ffffff; 623 | border-radius: 4px; 624 | display: table-cell; 625 | } 626 | 627 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .talking-palette { 628 | position: fixed; 629 | bottom: 32px; 630 | right: 32px; 631 | width: 168px; 632 | height: 56px; 633 | border-radius: 4px; 634 | border: 1px solid #BABABA; 635 | background-color: #ffffff; 636 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 637 | z-index: 20000; 638 | } 639 | 640 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .talking-palette-tools-container { 641 | position: absolute; 642 | left: 8px; 643 | right: 8px; 644 | top: 16px; 645 | bottom: 16px; 646 | border-collapse: separate; 647 | border-spacing: 8px 0; 648 | display: table; 649 | } 650 | 651 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-off-button-talking-palette { 652 | width: 24px; 653 | height: 24px; 654 | background-image: url(./resources/mic_on_palette.svg); 655 | background-size: 22px 22px; 656 | background-repeat: no-repeat; 657 | background-position: center; 658 | cursor: pointer; 659 | opacity: 1; 660 | display: table-cell; 661 | } 662 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-off-button-talking-palette:hover { 663 | opacity: .4; 664 | transition: opacity .5s; 665 | } 666 | 667 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-on-button-talking-palette { 668 | width: 24px; 669 | height: 24px; 670 | background-image: url(./resources/mic_off_palette.svg); 671 | background-size: 22px 22px; 672 | background-repeat: no-repeat; 673 | background-position: center; 674 | cursor: pointer; 675 | opacity: 1; 676 | display: table-cell; 677 | } 678 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mic-on-button-talking-palette:hover { 679 | opacity: .4; 680 | transition: opacity .5s; 681 | } 682 | 683 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hung-up-button-talking-palette { 684 | width: 24px; 685 | height: 24px; 686 | background-image: url(./resources/hang_up_palette.svg); 687 | background-size: 21px 21px; 688 | background-repeat: no-repeat; 689 | background-position: center; 690 | cursor: pointer; 691 | opacity: 1; 692 | display: table-cell; 693 | } 694 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .hung-up-button-talking-palette:hover { 695 | opacity: .4; 696 | transition: opacity .5s; 697 | } 698 | 699 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .maximize-button-talking-palette { 700 | width: 24px; 701 | height: 24px; 702 | background-image: url(./resources/maximize.svg); 703 | background-size: 21px 21px; 704 | background-repeat: no-repeat; 705 | background-position: center; 706 | cursor: pointer; 707 | opacity: 1; 708 | display: table-cell; 709 | } 710 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .maximize-button-talking-palette:hover { 711 | opacity: .4; 712 | transition: opacity .5s; 713 | } 714 | 715 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .share-display-button-talking-palette { 716 | width: 24px; 717 | height: 24px; 718 | background-image: url(./resources/share_display_palette.svg); 719 | background-size: 22px 22px; 720 | background-repeat: no-repeat; 721 | background-position: center; 722 | cursor: pointer; 723 | opacity: 1; 724 | display: table-cell; 725 | } 726 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .share-display-button-talking-palette:hover { 727 | opacity: .4; 728 | transition: opacity .5s; 729 | } 730 | 731 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .spacing-talking-palette { 732 | width: 42px; 733 | height: 24px; 734 | display: table-cell; 735 | } 736 | 737 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .add-member-button { 738 | position: absolute; 739 | left: 16px; 740 | bottom: 12px; 741 | right: 16px; 742 | height: 38px; 743 | background-color: #005bd5; 744 | background-image: url(./resources/plus.svg); 745 | background-size: 20px 20px; 746 | background-repeat: no-repeat; 747 | background-position: left 36px center; 748 | border-radius: 4px; 749 | cursor: pointer; 750 | } 751 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .add-member-button:hover { 752 | background-color: #003780; 753 | transition: background-color .5s; 754 | } 755 | 756 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .icon-button-text { 757 | position: absolute; 758 | left: calc(50% + 8px); 759 | top: 50%; 760 | transform: translate(-50%, -50%); 761 | color: #ffffff; 762 | font-size: 14px; 763 | font-weight: bold; 764 | white-space: nowrap; 765 | } 766 | 767 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-surface { 768 | position: absolute; 769 | width: 680px; 770 | height: 166px; 771 | left: 50%; 772 | top: 40%; 773 | transform: translate(-50%, -50%); 774 | background-color: #ffffff; 775 | border-radius: 4px; 776 | opacity: 0; 777 | animation-name: fadein; 778 | animation-duration: .5s; 779 | animation-timing-function: ease-out; 780 | animation-fill-mode: forwards; 781 | } 782 | 783 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-text { 784 | position: absolute; 785 | left: 56px; 786 | top: 40px; 787 | font-size: 16px; 788 | color: #000000; 789 | } 790 | 791 | 792 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-alert-optional-text-div { 793 | position: absolute; 794 | left: 56px; 795 | right: 56px; 796 | top: 82px; 797 | font-size: 16px; 798 | color: #5D5D5D; 799 | } 800 | 801 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-alert-optional-text { 802 | display: -webkit-box; 803 | -webkit-box-orient: vertical; 804 | -webkit-line-clamp: 2; 805 | overflow: hidden; 806 | } 807 | 808 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-cancel-button { 809 | position: absolute; 810 | left: 56px; 811 | bottom: 38px; 812 | width: 164px; 813 | height: 38px; 814 | background-color: #FF2747; 815 | background-image: url(./resources/hang_up.svg); 816 | background-size: 21px 21px; 817 | background-repeat: no-repeat; 818 | background-position: left 27px center; 819 | border-radius: 4px; 820 | cursor: pointer; 821 | } 822 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-cancel-button:hover { 823 | background-color: #bc0001; 824 | transition: background-color .5s; 825 | } 826 | 827 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-ok-button { 828 | position: absolute; 829 | left: 244px; 830 | bottom: 38px; 831 | width: 164px; 832 | height: 38px; 833 | background-color: #005BD5; 834 | background-image: url(./resources/hang_on.svg); 835 | background-size: 21px 21px; 836 | background-repeat: no-repeat; 837 | background-position: left 44px center; 838 | border-radius: 4px; 839 | cursor: pointer; 840 | } 841 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-accept-talking-alert-ok-button:hover { 842 | background-color: #003780; 843 | transition: background-color .5s; 844 | } 845 | 846 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-surface { 847 | position: absolute; 848 | width: 680px; 849 | height: 200px; 850 | left: 50%; 851 | top: 40%; 852 | transform: translate(-50%, -50%); 853 | background-color: #ffffff; 854 | border-radius: 4px; 855 | opacity: 0; 856 | animation-name: fadein; 857 | animation-duration: .5s; 858 | animation-timing-function: ease-out; 859 | animation-fill-mode: forwards; 860 | } 861 | 862 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-text { 863 | position: absolute; 864 | left: 56px; 865 | top: 40px; 866 | font-size: 16px; 867 | color: #000000; 868 | } 869 | 870 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-cancel-button { 871 | position: absolute; 872 | left: 56px; 873 | bottom: 38px; 874 | width: 164px; 875 | height: 38px; 876 | background-color: #FF2747; 877 | border-radius: 4px; 878 | cursor: pointer; 879 | } 880 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-cancel-button:hover { 881 | background-color: #bc0001; 882 | transition: background-color .5s; 883 | } 884 | 885 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-ok-button { 886 | position: absolute; 887 | left: 244px; 888 | bottom: 38px; 889 | width: 164px; 890 | height: 38px; 891 | background-color: #005BD5; 892 | border-radius: 4px; 893 | cursor: pointer; 894 | } 895 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .ask-stop-sharing-for-showing-talk-screen-alert-ok-button:hover { 896 | background-color: #003780; 897 | transition: background-color .5s; 898 | } 899 | 900 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .window-caption { 901 | position: absolute; 902 | left: 16px; 903 | top: 14px; 904 | font-size: 16px; 905 | color: #5D5D5D; 906 | font-weight: bold; 907 | } 908 | 909 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .window-caption-padding { 910 | position: absolute; 911 | left: 48px; 912 | top: 14px; 913 | font-size: 16px; 914 | color: #5D5D5D; 915 | font-weight: bold; 916 | } 917 | 918 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .window-table-container { 919 | position: absolute; 920 | left: 12px; 921 | top: 48px; 922 | right: 8px; 923 | bottom: 78px; 924 | overflow-y: auto; 925 | overflow-x: hidden; 926 | white-space: nowrap; 927 | } 928 | 929 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-window-close-button { 930 | position: absolute; 931 | top: 12px; 932 | right: 16px; 933 | width: 24px; 934 | height: 24px; 935 | background-image: url(./resources/close.svg); 936 | background-size: 14px 14px; 937 | background-repeat: no-repeat; 938 | background-position: center; 939 | cursor: pointer; 940 | opacity: 1; 941 | } 942 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-window-close-button:hover { 943 | opacity: .4; 944 | transition: opacity .5s; 945 | } 946 | 947 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-window-back-button { 948 | position: absolute; 949 | top: 12px; 950 | left: 16px; 951 | width: 24px; 952 | height: 24px; 953 | background-image: url(./resources/back.svg); 954 | background-size: 17px 16px; 955 | background-repeat: no-repeat; 956 | background-position: center; 957 | cursor: pointer; 958 | opacity: 1; 959 | } 960 | 961 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .member-window-back-button:hover { 962 | opacity: .4; 963 | transition: opacity .5s; 964 | } 965 | 966 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-joining-button-disabled { 967 | position: absolute; 968 | left: 16px; 969 | bottom: 12px; 970 | right: 16px; 971 | height: 38px; 972 | background-color: #BABABA; 973 | border-radius: 4px; 974 | cursor: pointer; 975 | } 976 | 977 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-joining-button { 978 | position: absolute; 979 | left: 16px; 980 | bottom: 12px; 981 | right: 16px; 982 | height: 38px; 983 | background-color: #005bd5; 984 | border-radius: 4px; 985 | cursor: pointer; 986 | } 987 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .request-joining-button:hover { 988 | background-color: #003780; 989 | transition: background-color .5s; 990 | } 991 | 992 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .mini-remote-videos-container { 993 | position: fixed; 994 | right: 20px; 995 | top: 116px; 996 | width: 256px; 997 | height: 144px; 998 | background-color: #1A1A1A; 999 | border-radius: 4px; 1000 | z-index: 10000; 1001 | } 1002 | 1003 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .stretch-screen-button { 1004 | position: fixed; 1005 | right: 20px; 1006 | top: 116px; 1007 | width: 32px; 1008 | height: 144px; 1009 | background-color: #333333; 1010 | background-image: url(./resources/stretch_screen.svg); 1011 | background-size: 20px 16px; 1012 | background-repeat: no-repeat; 1013 | background-position: center; 1014 | border-radius: 4px 0px 0px 4px; 1015 | z-index: 10000; 1016 | cursor: pointer; 1017 | } 1018 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .stretch-screen-button:hover { 1019 | background-color: #222222; 1020 | transition: background-color .5s; 1021 | } 1022 | 1023 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .maximize-screen-button { 1024 | position: absolute; 1025 | left: 68px; 1026 | top: 44px; 1027 | width: 56px; 1028 | height: 56px; 1029 | background-color: rgba(51, 51, 51, .7); 1030 | background-image: url(./resources/maximize_screen.svg); 1031 | background-size: 20px 16px; 1032 | background-repeat: no-repeat; 1033 | background-position: center; 1034 | border-radius: 50%; 1035 | cursor: pointer; 1036 | } 1037 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .maximize-screen-button:hover { 1038 | background-color: rgba(34, 34, 34, .7); 1039 | transition: background-color .5s; 1040 | } 1041 | 1042 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .minimize-screen-button { 1043 | position: absolute; 1044 | right: 68px; 1045 | top: 44px; 1046 | width: 56px; 1047 | height: 56px; 1048 | background-color: rgba(51, 51, 51, .7); 1049 | background-image: url(./resources/minimize_screen.svg); 1050 | background-size: 18px 16px; 1051 | background-repeat: no-repeat; 1052 | background-position: center; 1053 | border-radius: 50%; 1054 | cursor: pointer; 1055 | } 1056 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .minimize-screen-button:hover { 1057 | background-color: rgba(34, 34, 34, .7); 1058 | transition: background-color .5s; 1059 | } 1060 | 1061 | #CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-realtime-talk .remote-videos-description { 1062 | position: absolute; 1063 | left: 50%; 1064 | top: 50%; 1065 | transform: translate(-50%, -50%); 1066 | color: #BABABA; 1067 | font-size: 18px; 1068 | white-space: nowrap; 1069 | } -------------------------------------------------------------------------------- /nbwhisper/nbextension/main.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'base/js/namespace', 3 | 'jquery', 4 | 'require', 5 | 'base/js/events', 6 | 'base/js/utils', 7 | ], function(Jupyter, $, requirejs, events, utils) { 8 | "use strict"; 9 | 10 | const logPrefix = '[nbwhisper]'; 11 | 12 | const params = { 13 | nbwhisper_skyway_api_token: '', 14 | nbwhisper_room_mode_for_waiting_room: '', 15 | nbwhisper_room_mode_for_talking_room: '' 16 | }; 17 | 18 | const configure = async function() { 19 | // Apply server settings 20 | const server_config = await load_server_config(); 21 | if (!server_config.username) { 22 | throw new Error('Username not detected'); 23 | } 24 | own_user_name = own_user.name = server_config.username 25 | for(const key_ in params) { 26 | const m = key_.match(/^nbwhisper_(.+)$/); 27 | const key = m[1]; 28 | const value = server_config[key]; 29 | if (!value) { 30 | console.log(logPrefix, "param " + key + " is not set on server config"); 31 | continue; 32 | } 33 | console.log(logPrefix, "param = " + key + " value = " + value + " on server config"); 34 | params[key_] = value; 35 | } 36 | 37 | // Apply client settings 38 | const config = Jupyter.notebook.config; 39 | for(const key in params) { 40 | if(config.data.hasOwnProperty(key)) { 41 | const value = config.data[key]; 42 | if (!value) { 43 | console.log(logPrefix, "param " + key + " is not set"); 44 | continue; 45 | } 46 | console.log(logPrefix, "param = " + key + " value = " + value); 47 | params[key] = value; 48 | } 49 | } 50 | } 51 | 52 | var Peer; 53 | 54 | // 各要素のID 55 | const ID_PREFIX = "CBEE0ECE-42BD-475D-90F3-A9C2F2EC3191-"; // guidとして事前生成 56 | const ELEMENT_ID = ID_PREFIX + "realtime-talk"; 57 | const SHOW_USERS_BUTTON_ID = ID_PREFIX + "show-users-button"; 58 | const HIDE_USERS_BUTTON_ID = ID_PREFIX + "hide-users-button"; 59 | const USERS_LIST_DIALOG_ID = ID_PREFIX + "users-list-dialog"; 60 | const REQUEST_TALKING_BUTTON_ID = ID_PREFIX + "request-talking-button"; 61 | const REQUEST_JOINING_BUTTON_ID = ID_PREFIX + "request-joining-button"; 62 | const TALK_SCREEN_ID = ID_PREFIX + "talk-screen"; 63 | const TALKING_PALETTE_ID = ID_PREFIX + "talking-palette"; 64 | const TALK_SCREEN_MUTE_BUTTON_ID = ID_PREFIX + "talk-screen-mute-button"; 65 | const TALK_SCREEN_UNMUTE_BUTTON_ID = ID_PREFIX + "talk-screen-unmute-button"; 66 | const TALK_SCREEN_START_SHARE_DISPLAY_ID = ID_PREFIX + "talk-screen-start-share-display"; 67 | const TALK_SCREEN_STOP_SHARE_DISPLAY_ID = ID_PREFIX + "talk-screen-stop-share-display"; 68 | const TALK_SCREEN_SHOW_MEMBERS_BUTTON_ID = ID_PREFIX + "talk-screen-show-members-button"; 69 | const TALK_SCREEN_HIDE_MEMBERS_BUTTON_ID = ID_PREFIX + "talk-screen-hide-members-button"; 70 | const TALK_SCREEN_MEMBERS_WINDOW_ID = ID_PREFIX + "talk-screen-members-window"; 71 | const TALKING_PALETTE_MUTE_BUTTON_ID = ID_PREFIX + "talking-palette-mute-button"; 72 | const TALKING_PALETTE_UNMUTE_BUTTON_ID = ID_PREFIX + "talking-palette-unmute-button"; 73 | const TALKING_PALETTE_STOP_SHARE_DISPLAY_ID = ID_PREFIX + "talking-palette-stop-share-display"; 74 | const TALK_SCREEN_REMOTE_VIDEOS_CONTAINER_OUTER_ID = ID_PREFIX + "talk-screen-remote-videos-container-outer"; 75 | const TALK_SCREEN_REMOTE_VIDEOS_DESCRIPTION_ID = ID_PREFIX + "talk-screen-remote-videos-description"; 76 | const TALK_SCREEN_REMOTE_VIDEOS_CONTAINER_ID = ID_PREFIX + "talk-screen-remote-videos-container"; 77 | const TALK_SCREEN_SHARING_USER_NAME_TEXT_ID = ID_PREFIX + "talk-screen-shareing-user-name-text"; 78 | const USERS_TABLE_ID = ID_PREFIX + "users-table"; 79 | const ROOM_MEMBERS_TABLE_ID = ID_PREFIX + "room-members-table"; 80 | const TALK_SCREEN_USERS_TABLE_ID = ID_PREFIX + "talk-screen-users-table"; 81 | const DUMMY_VIDEO_CANVAS_ID = ID_PREFIX + "dummy-video-canvas"; 82 | const MINI_REMOTE_VIDEOS_CONTAINER_ID = ID_PREFIX + "mini-remote-videos-container"; 83 | const STRETCH_MINI_REMOTE_VIDEOS_ID = ID_PREFIX + "stretch-mini-remote-videos"; 84 | const TALK_SCREEN_ADD_MEMBERS_CONTAINER_ID = ID_PREFIX + "talk-screen-add-members-container"; 85 | const TALK_SCREEN_MEMBERS_LIST_ID = ID_PREFIX + "talk_screen-members-list"; 86 | 87 | // 各種メッセージ 88 | // ユーザーデータの更新 89 | const UPDATE_USER_DATA_MESSAGE = "update_user_data"; 90 | // ユーザーデータ更新に対するレスポンス 91 | const UPDATE_USER_DATA_RESPONSE_MESSAGE = "update_user_data_response"; 92 | // 通話への招待 93 | const INVITE_USER_MESSAGE = "invite_user"; 94 | // ミュート 95 | const MUTE_MESSAGE = "mute"; 96 | const UNMUTE_MESSAGE = "unmute"; 97 | // 画面共有 98 | const SHARE_START_MESSAGE = "share_start"; 99 | const SHARE_STOP_MESSAGE = "share_stop"; 100 | 101 | // 自身のユーザ名 102 | var own_user_name = ""; 103 | 104 | // 自身のユーザー情報及び他のユーザーのリスト 105 | // 前回との差分を取って変化があった時テーブルを更新したいため、保持しておく 106 | // name: 名前, is_mute: ミュートか, is_sharing_display: 画面共有中か, peer_id: PeerId, joining_room: 参加中のルーム, invited_rooms: 招待されている部屋 107 | var own_user = { 108 | name : "", 109 | is_mute : false, 110 | is_sharing_display : false, 111 | peer_id: null, 112 | joining_room : "", 113 | invited_rooms : [] 114 | }; 115 | // 全ユーザー情報 116 | // peer_id, joining_roomは複数存在する 117 | // name: 名前, is_mute: ミュートか, is_sharing_display: 画面共有中か, peer_id_to_joining_rooms: peer_idとjoining_roomの連想配列, invited_rooms: 招待されている部屋 118 | var other_users = []; 119 | // 自分が他のタブやウィンドウで開いているPeerの情報 120 | var own_other_peer_id_to_joining_rooms = {} 121 | 122 | // 選択ユーザー名 123 | var selected_user_names = []; 124 | // ローカルストリーム 125 | var local_stream = null; 126 | // ディスプレイストリーム 127 | var display_stream = null; 128 | // ピア 129 | var peer = null; 130 | // 待機ルーム 131 | var waiting_room = null; 132 | // 会話ルーム 133 | var talking_room = null; 134 | // ページアンロード処理フラグ 135 | var is_page_unloading = false; 136 | 137 | /** 138 | * load css file and append to document 139 | * 140 | * @method load_css 141 | * @param name {String} filenaame of CSS file 142 | * 143 | */ 144 | var load_css = function (name) { 145 | var link = document.createElement("link"); 146 | link.type = "text/css"; 147 | link.rel = "stylesheet"; 148 | link.href = requirejs.toUrl(name); 149 | document.getElementsByTagName("head")[0].appendChild(link); 150 | }; 151 | 152 | var load_js_async = function (url) { 153 | return new Promise(function(resolve) { 154 | requirejs([url], function(lib) { 155 | resolve(lib); 156 | }); 157 | }); 158 | } 159 | 160 | // ルームが存在しているので開始できないアラートを表示 161 | var show_room_existed_alert = function() { 162 | alert("他のタブやウィンドウで通話中のため、新たに通話を開始することができませんでした"); 163 | } 164 | 165 | // マイクがキャンセルされた 166 | var show_mic_cancelled_alert = function() { 167 | alert("マイクを使用することができないため、通話を開始することができませんでした"); 168 | } 169 | 170 | // 招待は無効(消去された場合。実装してない) 171 | var show_unavailable_invitation_alert = function() { 172 | alert("この招待は無効です") 173 | } 174 | 175 | // 通話が終了したため招待が無効 176 | var show_unavailable_invitation_with_finished_talk_alert = function() { 177 | alert("通話が終了したため、この招待は無効になりました") 178 | } 179 | 180 | // ランダムでuuidを生成する 181 | const generate_uuid = function() { 182 | // https://github.com/GoogleChrome/chrome-platform-analytics/blob/master/src/internal/identifier.js 183 | // const FORMAT: string = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; 184 | let chars = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".split(""); 185 | for (let i = 0, len = chars.length; i < len; i++) { 186 | switch (chars[i]) { 187 | case "x": 188 | chars[i] = Math.floor(Math.random() * 16).toString(16); 189 | break; 190 | case "y": 191 | chars[i] = (Math.floor(Math.random() * 4) + 8).toString(16); 192 | break; 193 | } 194 | } 195 | return chars.join(""); 196 | } 197 | 198 | // 非同期メソッドを同期かつ排他で実行する 199 | var is_busy_in_process = false; 200 | var invoke_async_process = function(func) { 201 | if(is_busy_in_process) return; 202 | console.log(logPrefix, "process start."); 203 | is_busy_in_process = true; 204 | func().then(() => { 205 | is_busy_in_process = false; 206 | console.log(logPrefix, "process end."); 207 | }); 208 | } 209 | 210 | const load_server_config = function() { 211 | return new Promise(function(resolve, reject) { 212 | const path = Jupyter.notebook.base_url + "nbwhisper/v1/config"; 213 | $.get(path).done(function(data) { 214 | console.log(logPrefix, 'config', data); 215 | resolve(data); 216 | }).fail(function(xhr, status, error) { 217 | reject(error); 218 | }); 219 | }); 220 | } 221 | 222 | // ダミーの映像ストリーム用キャンバスの作成 223 | var create_dummy_video_canvas = function(width = 640, height = 640) { 224 | let canvas = $("").css("position", "fixed").css("top", "100vh"); 225 | let el = canvas.get(0); 226 | el.width = width; 227 | el.height = height; 228 | let draw_loop = function () { 229 | el.getContext('2d').fillRect(0, 0, width, height); 230 | requestAnimationFrame(draw_loop); 231 | } 232 | draw_loop(); 233 | return canvas; 234 | } 235 | 236 | var get_offset_right = function(jqObject) { 237 | return $(window).width() - (jqObject.offset().left + jqObject.outerWidth()); 238 | } 239 | 240 | var get_offset_bottom = function(jqObject) { 241 | return $(window).height() - (jqObject.offset().top + jqObject.outerHeight()); 242 | } 243 | 244 | var setup_move_and_resize_dialog = function(dialog) { 245 | // ウィンドウ移動 246 | dialog.mousedown(function(e) { 247 | dialog.data("clickPageX", e.pageX); 248 | dialog.data("clickPageY", e.pageY); 249 | dialog.data("offsetRight", get_offset_right(dialog)); 250 | dialog.data("offsetBottom", get_offset_bottom(dialog)); 251 | $(document).mousemove(function(e) { 252 | dialog.css({ 253 | right: dialog.data("offsetRight") - (e.pageX - dialog.data("clickPageX")) + "px", 254 | bottom: dialog.data("offsetBottom") - (e.pageY - dialog.data("clickPageY")) + "px", 255 | }); 256 | return false; 257 | }); 258 | $(document).mouseup(function(e) { 259 | $(document).unbind("mousemove"); 260 | $(document).unbind("mouseup"); 261 | return false; 262 | }); 263 | return false; 264 | }); 265 | // ウィンドウリサイズ 266 | let resize_bar_left = $("
").addClass("resize-dialog-bar-left").appendTo(dialog); 267 | let resize_bar_top = $("
").addClass("resize-dialog-bar-top").appendTo(dialog); 268 | let resize_bar_right = $("
").addClass("resize-dialog-bar-right").appendTo(dialog); 269 | let resize_bar_bottom = $("
").addClass("resize-dialog-bar-bottom").appendTo(dialog); 270 | let resize_bar_topleft = $("
").addClass("resize-dialog-bar-topleft").appendTo(dialog); 271 | let resize_bar_topright = $("
").addClass("resize-dialog-bar-topright").appendTo(dialog); 272 | let resize_bar_bottomright = $("
").addClass("resize-dialog-bar-bottomright").appendTo(dialog); 273 | let resize_bar_bottomleft = $("
").addClass("resize-dialog-bar-bottomleft").appendTo(dialog); 274 | for (let bar of [resize_bar_left, resize_bar_top, resize_bar_right, resize_bar_bottom, 275 | resize_bar_topleft, resize_bar_topright, resize_bar_bottomright, resize_bar_bottomleft]) { 276 | $(bar).mousedown(function(e) { 277 | dialog.data("clickPageX", e.pageX); 278 | dialog.data("clickPageY", e.pageY); 279 | dialog.data("offsetRight", get_offset_right(dialog)); 280 | dialog.data("offsetBottom", get_offset_bottom(dialog)); 281 | dialog.data("clientWidth", dialog.outerWidth()); 282 | dialog.data("clientHeight", dialog.outerHeight()); 283 | $(document).mousemove(function(e) { 284 | let deltaX = e.pageX - dialog.data("clickPageX"); 285 | let deltaY = e.pageY - dialog.data("clickPageY"); 286 | const DIALOG_SIZE_MIN = 120; 287 | if(bar == resize_bar_right || bar == resize_bar_topright || bar == resize_bar_bottomright) { 288 | // 幅、右を変更 289 | let newWidth = dialog.data("clientWidth") + deltaX; 290 | if(newWidth >= DIALOG_SIZE_MIN) { 291 | $(dialog).css({ 292 | right: dialog.data("offsetRight") - deltaX + "px", 293 | width: newWidth + "px" 294 | }); 295 | } 296 | } 297 | if(bar == resize_bar_left || bar == resize_bar_topleft || bar == resize_bar_bottomleft) { 298 | // 幅を逆方向に変更 299 | let newWidth = dialog.data("clientWidth") - deltaX; 300 | if(newWidth >= DIALOG_SIZE_MIN) { 301 | $(dialog).css({ 302 | width: newWidth + "px" 303 | }); 304 | } 305 | } 306 | if(bar == resize_bar_bottom || bar == resize_bar_bottomright || bar == resize_bar_bottomleft) { 307 | // 高さ、下を変更 308 | let newHeight = dialog.data("clientHeight") + deltaY; 309 | if(newHeight >= DIALOG_SIZE_MIN) { 310 | $(dialog).css({ 311 | bottom: dialog.data("offsetBottom") - deltaY + "px", 312 | height: newHeight + "px" 313 | }); 314 | } 315 | } 316 | if(bar == resize_bar_top || bar == resize_bar_topleft || bar == resize_bar_topright) { 317 | // 高さを逆方向に変更 318 | let newHeight = dialog.data("clientHeight") - deltaY; 319 | if(newHeight >= DIALOG_SIZE_MIN) { 320 | $(dialog).css({ 321 | height: newHeight + "px" 322 | }); 323 | } 324 | } 325 | return false; 326 | }) 327 | $(document).mouseup(function(e) { 328 | $(document).unbind("mousemove"); 329 | $(document).unbind("mouseup"); 330 | return false; 331 | }); 332 | return false; 333 | }); 334 | } 335 | } 336 | 337 | // ユーザーリスト表示 338 | var show_user_list = function() { 339 | $("#"+SHOW_USERS_BUTTON_ID).hide(); 340 | $("#"+HIDE_USERS_BUTTON_ID).show(); 341 | $("#"+USERS_LIST_DIALOG_ID).fadeIn("fast"); 342 | } 343 | 344 | // ユーザーリスト隠す 345 | var hide_user_list = function() { 346 | $("#"+SHOW_USERS_BUTTON_ID).show(); 347 | $("#"+HIDE_USERS_BUTTON_ID).hide(); 348 | $("#"+USERS_LIST_DIALOG_ID).fadeOut("fast"); 349 | } 350 | 351 | // マイクをミュートにする 352 | var set_mute = async function(mute) { 353 | // ボタン変更 354 | update_mic_button(mute); 355 | // メッセージ送信 356 | own_user.is_mute = mute; 357 | send_message_to_talking_room(mute ? MUTE_MESSAGE : UNMUTE_MESSAGE); 358 | // ストリーム変更 359 | set_stream_mute(mute); 360 | // テーブル更新 361 | update_tables(); 362 | } 363 | 364 | var update_mic_button = function(mute) { 365 | if(mute) { 366 | $("#"+TALK_SCREEN_MUTE_BUTTON_ID).hide(); 367 | $("#"+TALK_SCREEN_UNMUTE_BUTTON_ID).show(); 368 | $("#"+TALKING_PALETTE_MUTE_BUTTON_ID).hide(); 369 | $("#"+TALKING_PALETTE_UNMUTE_BUTTON_ID).show(); 370 | } else { 371 | $("#"+TALK_SCREEN_MUTE_BUTTON_ID).show(); 372 | $("#"+TALK_SCREEN_UNMUTE_BUTTON_ID).hide(); 373 | $("#"+TALKING_PALETTE_MUTE_BUTTON_ID).show(); 374 | $("#"+TALKING_PALETTE_UNMUTE_BUTTON_ID).hide(); 375 | } 376 | } 377 | 378 | var set_stream_mute = function(mute) { 379 | if(local_stream != null) { 380 | let track = local_stream.getAudioTracks()[0] 381 | if(track != null) { 382 | console.log(logPrefix, "local_stream 音声のミュート状態変更 -> " + mute); 383 | track.enabled = !mute; 384 | } 385 | } 386 | if(display_stream != null) { 387 | let track = display_stream.getAudioTracks()[0] 388 | if(track != null) { 389 | console.log(logPrefix, "display_stream 音声のミュート状態変更 -> " + mute); 390 | track.enabled = !mute; 391 | } 392 | } 393 | } 394 | 395 | // 画面の共有をオンにする 396 | var set_share_display_on_async = async function() { 397 | // ONにする前にいったんこのビューを最小化する 398 | await hide_view_to_bottom_right_async($("#"+TALK_SCREEN_ID)); 399 | // ミニ動画共有コンテナセットアップ 400 | setup_mini_remote_videos(); 401 | // 共有開始処理 402 | let new_stream = await start_share_display_async(); 403 | if(new_stream != null) { 404 | // ボタン更新 405 | update_share_display_button(true); 406 | // 共有開始メッセージをルームに送る 407 | send_message_to_talking_room(SHARE_START_MESSAGE); 408 | // ストリームを保持する 409 | display_stream = new_stream; 410 | // テーブル更新 411 | own_user.is_sharing_display = true; 412 | update_tables(); 413 | update_remote_videos(); 414 | } else { 415 | // ビューを元に戻す 416 | show_view_from_bottom_right($("#"+TALK_SCREEN_ID)); 417 | dispose_mini_remote_videos(); 418 | } 419 | } 420 | 421 | // 画面の共有をオフにする 422 | var set_share_display_off = function() { 423 | // ボタン更新 424 | update_share_display_button(false); 425 | // 共有停止メッセージをルームに送る 426 | send_message_to_talking_room(SHARE_STOP_MESSAGE); 427 | // 共有停止処理 428 | stop_share_display(); 429 | // テーブル更新 430 | own_user.is_sharing_display = false; 431 | update_tables(); 432 | // 画面更新 433 | update_remote_videos(); 434 | } 435 | 436 | var update_share_display_button = function(share_display) { 437 | if(share_display) { 438 | $("#"+TALKING_PALETTE_STOP_SHARE_DISPLAY_ID).show(); 439 | $("#"+TALK_SCREEN_START_SHARE_DISPLAY_ID).hide(); 440 | $("#"+TALK_SCREEN_STOP_SHARE_DISPLAY_ID).show(); 441 | $("#"+TALKING_PALETTE_ID).css("width", "200px"); 442 | } else { 443 | $("#"+TALKING_PALETTE_STOP_SHARE_DISPLAY_ID).hide(); 444 | $("#"+TALK_SCREEN_START_SHARE_DISPLAY_ID).show(); 445 | $("#"+TALK_SCREEN_STOP_SHARE_DISPLAY_ID).hide(); 446 | $("#"+TALKING_PALETTE_ID).css("width", ""); 447 | } 448 | } 449 | 450 | // 画面共有を開始する 451 | // return: 生成した画面共有ストリーム 452 | var start_share_display_async = async function() { 453 | try { 454 | const stream = await navigator.mediaDevices.getDisplayMedia({ 455 | video: true, 456 | selfBrowserSurface: 'include', 457 | preferCurrentTab: true 458 | }); 459 | if(stream == null) return null; 460 | // ストリームの映像を差し替える 461 | let audio_track = local_stream.getAudioTracks()[0]; 462 | let video_track = stream.getVideoTracks()[0]; 463 | // 映像ストリーム生成 464 | let new_stream = new MediaStream([video_track, audio_track]); 465 | // 差し替える 466 | talking_room.replaceStream(new_stream); 467 | // 古いローカルストリームを閉じる 468 | if(local_stream != null) { 469 | local_stream.getVideoTracks().forEach(t => t.stop()); 470 | local_stream = null; 471 | } 472 | 473 | // 自身のストリームを表示するビデオタグを探す 474 | let my_video = null; 475 | for(let v of $("#"+TALK_SCREEN_REMOTE_VIDEOS_CONTAINER_ID).children()) { 476 | if($(v).hasClass("remote-video-shown")) { 477 | // 表示中のビデオがあったら非表示にしておく 478 | $(v).removeClass("remote-video-shown").addClass("remote-video-hidden"); 479 | } 480 | if($(v).data("peerId") == own_user.peer_id) { 481 | my_video = $(v); 482 | } 483 | } 484 | if(my_video == null) { 485 | // ない場合は追加する 486 | my_video = $("