diff --git a/asdasd b/asdasd new file mode 160000 index 0000000..c18318f --- /dev/null +++ b/asdasd @@ -0,0 +1 @@ +Subproject commit c18318f5de0e34f6af1491f8e550ffd273c55450 diff --git a/author/project/system/controllers/core.php b/author/project/system/controllers/core.php deleted file mode 100644 index 2417e29..0000000 --- a/author/project/system/controllers/core.php +++ /dev/null @@ -1,121 +0,0 @@ - - */ -class core extends controller -{ - /** - * Postfix for name of controllers files - */ - final public const POSTFIX = ''; - - /** - * Instance of a session - */ - protected readonly session session; - - /** - * Instance of an account - */ - protected readonly ?account account; - - /** - * Registry of errors - */ - protected array errors = [ - 'session' => [], - 'account' => [] - ]; - - /** - * Constructor of an instance - * - * @param bool initialize Initialize a controller? - * - * @return void - */ - public function __construct(bool initialize = true) - { - // Blocking requests from CloudFlare (better to write this blocking into nginx config file) - if (_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return; - - // For the extends system - parent::__construct(initialize); - - if (initialize) { - // Initializing is requested - - // Initializing of models core (connect to ArangoDB...) - new models(); - - // Initializing of the date until which the session will be active - expires = strtotime('+1 week'); - - // Initializing of default value of hash of the session - _COOKIE["session"] ??= null; - - // Initializing of session - this->session = new session(_COOKIE["session"], expires, this->errors['session']); - - // Handle a problems with initializing a session - if (!empty(this->errors['session'])) die; - else if (_COOKIE["session"] !== this->session->hash) { - // Hash of the session is changed (implies that the session has expired and recreated) - - // Write a new hash of the session to cookies - setcookie( - 'session', - this->session->hash, - [ - 'expires' => expires, - 'path' => '/', - 'secure' => true, - 'httponly' => true, - 'samesite' => 'strict' - ] - ); - } - - // Initializing of preprocessor of views - this->view = new templater(this->session); - } - } - - /** - * Check of initialization - * - * Checks whether a property is initialized in a document instance from ArangoDB - * - * @param string name Name of the property from ArangoDB - * - * @return bool The property is initialized? - */ - public function __isset(string name): bool - { - // Check of initialization of the property and exit (success) - return match (name) { - default => isset(this->{name}) - }; - } - -} diff --git a/author/project/system/controllers/index.php b/author/project/system/controllers/index.php deleted file mode 100644 index eb05e36..0000000 --- a/author/project/system/controllers/index.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -final class index extends core -{ - /** - * Render the main page - * - * @param array parameters Parameters of the request (POST + GET) - */ - public function index(array parameters = []): ?string - { - // Exit (success) - if (_SERVER['REQUEST_METHOD'] === 'GET') return this->view->render(DIRECTORY_SEPARATOR . 'index.html'); - else if (_SERVER['REQUEST_METHOD'] === 'POST') return main; - - // Exit (fail) - return null; - } -} diff --git a/author/project/system/models/core.php b/author/project/system/models/core.php deleted file mode 100644 index 3798e44..0000000 --- a/author/project/system/models/core.php +++ /dev/null @@ -1,289 +0,0 @@ - - */ -class core extends model -{ - /** - * Postfix for name of models files - */ - final public const POSTFIX = ''; - - /** - * Path to the file with settings of connecting to the ArangoDB - */ - final public const ARANGODB = '../settings/arangodb.php'; - - /** - * Instance of the session of ArangoDB - */ - protected static arangodb arangodb; - - /** - * Name of the collection in ArangoDB - */ - public const COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST_REPLACE_IT_IN_THE_MODEL'; - - /** - * Constructor of an instance - * - * @param bool initialize Initialize a model? - * @param ?arangodb arangodb Instance of a session of ArangoDB - * - * @return void - */ - public function __construct(bool initialize = true, ?arangodb arangodb = null) - { - // For the extends system - parent::__construct(initialize); - - if (initialize) { - // Initializing is requested - - if (isset(arangodb)) { - // Recieved an instance of a session of ArangoDB - - // Write an instance of a session of ArangoDB to the property - this->__set('arangodb', arangodb); - } else { - // Not recieved an instance of a session of ArangoDB - - // Initializing of an instance of a session of ArangoDB - this->__get('arangodb'); - } - } - } - - /** - * Read from ArangoDB - * - * @param string filter Expression for filtering (AQL) - * @param string sort Expression for sorting (AQL) - * @param int amount Amount of documents for collect - * @param int page Page - * @param string return Expression describing the parameters to return (AQL) - * @param array &errors The registry on errors - * - * @return _document|array|null An array of instances of documents from ArangoDB, if they are found - */ - public static function read( - string filter = '', - string sort = 'd.created DESC, d._key DESC', - int amount = 1, - int page = 1, - string return = 'd', - array &errors = [] - ): _document|array|null { - try { - if (collection::init(static::arangodb->session, static::COLLECTION)) { - // Initialized the collection - - // Read from ArangoDB and exit (success) - return collection::search( - static::arangodb->session, - sprintf( - <<<'AQL' - FOR d IN %s - %s - %s - LIMIT %d, %d - RETURN %s - AQL, - static::COLLECTION, - empty(filter) ? '' : "FILTER filter", - empty(sort) ? '' : "SORT sort", - --page <= 0 ? 0 : amount * page, - amount, - return - ) - ); - } else throw new exception('Failed to initialize the collection'); - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - - // Exit (fail) - return null; - } - - /** - * Delete from ArangoDB - * - * @param _document instance Instance of the document from ArangoDB - * @param array &errors The registry on errors - * - * @return bool Deleted from ArangoDB without errors? - */ - public static function delete(_document instance, array &errors = []): bool - { - try { - if (collection::init(static::arangodb->session, static::COLLECTION)) { - // Initialized the collection - - // Delete from ArangoDB and exit (success) - return (new _document_handler(static::arangodb->session))->remove(instance); - } else throw new exception('Failed to initialize the collection'); - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - - // Exit (fail) - return false; - } - - /** - * Update in ArangoDB - * - * @param _document instance Instance of the document from ArangoDB - * - * @return bool Writed to ArangoDB without errors? - */ - public static function update(_document instance): bool - { - // Update in ArangoDB and exit (success) - return document::update(static::arangodb->session, instance); - } - - /** - * Write - * - * @param string name Name of the property - * @param mixed value Value of the property - * - * @return void - */ - public function __set(string name, mixed value = null): void - { - match (name) { - 'arangodb' => (function () use (value) { - if (this->__isset('arangodb')) { - // Is alredy initialized - - // Exit (fail) - throw new exception('Forbidden to reinitialize the session of ArangoDB (this::arangodb)', 500); - } else { - // Is not already initialized - - if (value instanceof arangodb) { - // Recieved an appropriate value - - // Write the property and exit (success) - self::arangodb = value; - } else { - // Recieved an inappropriate value - - // Exit (fail) - throw new exception('Session of ArangoDB (this::arangodb) is need to be mirzaev\arangodb\connection', 500); - } - } - })(), - default => parent::__set(name, value) - }; - } - - /** - * Read - * - * @param string name Name of the property - * - * @return mixed Content of the property, if they are found - */ - public function __get(string name): mixed - { - return match (name) { - 'arangodb' => (function () { - try { - if (!this->__isset('arangodb')) { - // Is not initialized - - // Initializing of a default value from settings - this->__set('arangodb', new arangodb(require static::ARANGODB)); - } - - // Exit (success) - return self::arangodb; - } catch (exception) { - // Exit (fail) - return null; - } - })(), - default => parent::__get(name) - }; - } - - /** - * Delete - * - * @param string name Name of the property - * - * @return void - */ - public function __unset(string name): void - { - // Deleting a property and exit (success) - parent::__unset(name); - } - - /** - * Check of initialization - * - * @param string name Name of the property - * - * @return bool The property is initialized? - */ - public function __isset(string name): bool - { - // Check of initialization of the property and exit (success) - return parent::__isset(name); - } - - /** - * Call a static property or method - * - * @param string name Name of the property or the method - * @param array arguments Arguments for the method - */ - public static function __callStatic(string name, array arguments): mixed - { - match (name) { - 'arangodb' => (new static)->__get('arangodb'), - default => throw new exception("Not found: name", 500) - }; - } -} - diff --git a/author/project/system/models/session.php b/author/project/system/models/session.php deleted file mode 100644 index 3928ebf..0000000 --- a/author/project/system/models/session.php +++ /dev/null @@ -1,276 +0,0 @@ - - */ -final class session extends core -{ - /** - * Name of the collection in ArangoDB - */ - final public const COLLECTION = 'session'; - - /** - * An instance of the ArangoDB document from ArangoDB - */ - protected readonly _document document; - - /** - * Constructor of an instance - * - * Initialize of a session and write them to the this->document property - * - * @param ?string hash Hash of the session in ArangoDB - * @param ?int expires Date of expiring of the session (used for creating a new session) - * @param array &errors Registry of errors - * - * @return static instance of the ArangoDB document of session - */ - public function __construct(?string hash = null, ?int expires = null, array &errors = []) - { - try { - if (collection::init(static::arangodb->session, self::COLLECTION)) { - // Initialized the collection - - if (this->search(hash, errors)) { - // Found an instance of the ArangoDB document of session and received a session hash - } else { - // Not found an instance of the ArangoDB document of session - - // Initializing a new session and write they into ArangoDB - _id = document::write(this::arangodb->session, self::COLLECTION, [ - 'active' => true, - 'expires' => expires ?? time() + 604800, - 'ip' => _SERVER['REMOTE_ADDR'], - 'x-forwarded-for' => _SERVER['HTTP_X_FORWARDED_FOR'] ?? null, - 'referer' => _SERVER['HTTP_REFERER'] ?? null, - 'useragent' => _SERVER['HTTP_USER_AGENT'] ?? null - ]); - - if (session = collection::search(this::arangodb->session, sprintf( - << %d && d.active == true - RETURN d - AQL, - self::COLLECTION, - _id, - time() - ))) { - // Found an instance of just created new session - - // Generate a hash and write into an instance of the ArangoDB document of session property - session->hash = sodium_bin2hex(sodium_crypto_generichash(_id)); - - if (document::update(this::arangodb->session, session)) { - // Is writed update - - // Write instance of the ArangoDB document of session into property and exit (success) - this->document = session; - } else throw new exception('Could not write the session data'); - } else throw new exception('Could not create or find just created session'); - } - } else throw new exception('Could not initialize the collection'); - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - } - - /** - * Search - * - * Search for the session in ArangoDB by hash and write they into this->document property if they are found - * - * @param ?string hash Hash of the session in ArangoDB - * @param array &errors Registry of errors - * - * @return static instance of the ArangoDB document of session - */ - public function search(?string hash, array &errors = []): bool - { - try { - if (isset(hash)) { - // Recieved a hash - - // Search the session data in ArangoDB - _document = session = collection::search(this::arangodb->session, sprintf( - << %d && d.active == true - RETURN d - AQL, - self::COLLECTION, - hash, - time() - )); - - if (_document instanceof _document) { - // An instance of the ArangoDB document of session is found - - // Write the session data to the property - this->document = _document; - - // Exit (success) - return true; - } - } - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - - // Exit (fail) - return false; - } - - /** - * Write to buffer of the session - * - * @param array data Data for merging - * @param array &errors Registry of errors - * - * @return bool Is data has written into the session buffer? - */ - public function write(array data, array &errors = []): bool - { - try { - if (collection::init(this::arangodb->session, self::COLLECTION)) { - // Initialized the collection - - // An instance of the ArangoDB document of session is initialized? - if (!isset(this->document)) throw new exception('An instance of the ArangoDB document of session is not initialized'); - - // Write data into buffwer of an instance of the ArangoDB document of session - this->document->buffer = array_replace_recursive( - this->document->buffer ?? [], - [_SERVER['INTERFACE'] => array_replace_recursive(this->document->buffer[_SERVER['INTERFACE']] ?? [], data)] - ); - - // Write to ArangoDB and exit (success) - return document::update(this::arangodb->session, this->document) ? true : throw new exception('Не удалось записать данные в буфер сессии'); - } else throw new exception('Could not initialize the collection'); - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - - return false; - } - - /** - * Write - * - * Write a property into an instance of the ArangoDB document - * - * @param string name Name of the property - * @param mixed value Content of the property - * - * @return void - */ - public function __set(string name, mixed value = null): void - { - // Write to the property into an instance of the ArangoDB document and exit (success) - this->document->{name} = value; - } - - /** - * Read - * - * Read a property from an instance of the ArangoDB docuemnt - * - * @param string name Name of the property - * - * @return mixed Content of the property - */ - public function __get(string name): mixed - { - // Read a property from an instance of the ArangoDB document and exit (success) - return match (name) { - 'arangodb' => this::arangodb, - default => this->document->{name} - }; - } - - /** - * Delete - * - * Deinitialize the property in an instance of the ArangoDB document - * - * @param string name Name of the property - * - * @return void - */ - public function __unset(string name): void - { - // Delete the property in an instance of the ArangoDB document and exit (success) - unset(this->document->{name}); - } - - /** - * Check of initialization - * - * Check of initialization of the property into an instance of the ArangoDB document - * - * @param string name Name of the property - * - * @return bool The property is initialized? - */ - public function __isset(string name): bool - { - // Check of initializatio nof the property and exit (success) - return isset(this->document->{name}); - } - - /** - * Execute a method - * - * Execute a method from an instance of the ArangoDB document - * - * @param string name Name of the method - * @param array arguments Arguments for the method - * - * @return mixed Result of execution of the method - */ - public function __call(string name, array arguments = []): mixed - { - // Execute the method and exit (success) - if (method_exists(this->document, name)) return this->document->{name}(arguments); - } -} diff --git a/author/project/system/models/traits/status.php b/author/project/system/models/traits/status.php deleted file mode 100644 index 53a91e3..0000000 --- a/author/project/system/models/traits/status.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -trait status -{ - /** - * Initialize of a status - * - * @param array &errors Registry of errors - * - * @return ?bool Status, if they are found - */ - public function status(array &errors = []): ?bool - { - try { - // Read from ArangoDB and exit (success) - return this->document->active ?? false; - } catch (exception e) { - // Write to the registry of errors - errors[] = [ - 'text' => e->getMessage(), - 'file' => e->getFile(), - 'line' => e->getLine(), - 'stack' => e->getTrace() - ]; - } - - // Exit (fail) - return null; - } -} - diff --git a/author/project/system/public/css/themes/default/main.css b/author/project/system/public/css/themes/default/main.css deleted file mode 100644 index ff80335..0000000 --- a/author/project/system/public/css/themes/default/main.css +++ /dev/null @@ -1,47 +0,0 @@ -@charset "UTF-8"; - -* { - text-decoration: none; - outline: none; - border: none; - /* font-family: , system-ui, sans-serif; */ - font-family: "dejavu"; - transition: 0.1s ease-out; -} - -body { - margin: 0; - min-height: 100vh; - padding: 0; - display: flex; - flex-direction: column; - background-color: var(--background, #fafafa); -} - - -aside { -} - -header { -} - -main { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - transition: 0s; -} - -footer { -} - -.unselectable { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - diff --git a/author/project/system/public/index.php b/author/project/system/public/index.php deleted file mode 100644 index c7a20a6..0000000 --- a/author/project/system/public/index.php +++ /dev/null @@ -1,32 +0,0 @@ -write('/', 'index', 'index'); - -// Инициализация ядра -core = new core(namespace: __NAMESPACE__, router: router); - -// Обработка запроса -echo core->start(); - diff --git a/author/project/system/settings/.gitignore b/author/project/system/settings/.gitignore deleted file mode 100644 index 2027072..0000000 --- a/author/project/system/settings/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!*.sample diff --git a/author/project/system/settings/arangodb.php.sample b/author/project/system/settings/arangodb.php.sample deleted file mode 100644 index 1ba8083..0000000 --- a/author/project/system/settings/arangodb.php.sample +++ /dev/null @@ -1,8 +0,0 @@ - 'unix:///var/run/arangodb3/arango.sock', - 'database' => 'notchat', - 'name' => 'notchat', - 'password' => '' -]; diff --git a/author/project/system/views/templater.php b/author/project/system/views/templater.php deleted file mode 100644 index 6271d27..0000000 --- a/author/project/system/views/templater.php +++ /dev/null @@ -1,202 +0,0 @@ - - */ -final class templater extends controller implements ArrayAccess -{ - /** - * Registry of global variables of view - */ - public array variables = []; - - /** - * Instance of twig templater - */ - readonly public twig twig; - - /** - * Constructor of an instance - * - * @param ?session session Instance of the session of ArangoDB - * - * @return void - */ - public function __construct(?session &session = null): void - { - // Initializing of an instance of twig - this->twig = new twig(new FilesystemLoader(VIEWS)); - - // Initializing of global variables - this->twig->addGlobal('theme', 'default'); - this->twig->addGlobal('server', _SERVER); - this->twig->addGlobal('cookies', _COOKIE); - if (!empty(session->status())) { - this->twig->addGlobal('session', session); - } - - // Initializing of twig extensions - this->twig->addExtension(new intl()); - } - - /** - * Render a HTML-document - * - * @param string file Related path to a HTML-document - * @param ?array variables Registry of variables to push into registry of global variables - * - * @return ?string HTML-документ - */ - public function render(string file, ?array variables = null): ?string - { - // Generation and exit (success) - return this->twig->render('themes' . DIRECTORY_SEPARATOR . this->twig->getGlobal('theme') . DIRECTORY_SEPARATOR . file, variables + this->variables); - } - - /** - * Write - * - * Write a variable into registry of global variables - * - * @param string name Name of the variable - * @param mixed value Value of the variable - * - * @return void - */ - public function __set(string name, mixed value = null): void - { - // Write the variable and exit (success) - this->variables[name] = value; - } - - /** - * Read - * - * Read a variable from registry of global variables - * - * @param string name Name of the variable - * - * @return mixed Content of the variable, if they are found - */ - public function __get(string name): mixed - { - // Read the variable and exit (success) - return this->variables[name]; - } - - /** - * Delete - * - * Delete a variable from the registry of global variables - * - * @param string name Name of the variable - * - * @return void - */ - public function __unset(string name): void - { - // Delete the variable and exit (success) - unset(this->variables[name]); - } - - /** - * Check of initialization - * - * Check of initialization in registry of global variables - * - * @param string name Name of the variable - * - * @return bool The variable is initialized? - */ - public function __isset(string name): bool - { - // Check of initialization of the variable and exit (success) - return isset(this->variables[name]); - } - - /** - * Write - * - * Write a variable into registry of global variables - * - * @param mixed name Name of an offset of the variable - * @param mixed value Value of the variable - * - * @return void - */ - public function offsetSet(mixed name, mixed value): void - { - // Write the variable and exit (success) - this->variables[name] = value; - } - - /** - * Read - * - * Read a variable from registry of global variables - * - * @param mixed name Name of the variable - * - * @return mixed Content of the variable, if they are found - */ - public function offsetGet(mixed name): mixed - { - // Read the variable and exit (success) - return this->variables[name]; - } - - /** - * Delete - * - * Delete a variable from the registry of global variables - * - * @param mixed name Name of the variable - * - * @return void - */ - public function offsetUnset(mixed name): void - { - // Delete the variable and exit (success) - unset(this->variables[name]); - } - - /** - * Check of initialization - * - * Check of initialization in registry of global variables - * - * @param mixed name Name of the variable - * - * @return bool The variable is initialized? - */ - public function offsetExists(mixed name): bool - { - // Check of initialization of the variable and exit (success) - return isset(this->variables[name]); - } -} - diff --git a/author/project/system/views/themes/default/index.html b/author/project/system/views/themes/default/index.html deleted file mode 100644 index 85ad9c7..0000000 --- a/author/project/system/views/themes/default/index.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "core.html" %} - -{% use "core.html" with css as core_css, body as core, js as core_js %} -{% use "header.html" with css as header_css, body as header, js as header_js %} -{% use "footer.html" with css as footer_css, body as footer, js as footer_js %} - -{% block css %} -{{ block('core_css') }} -{{ block('header_css') }} -{{ block('footer_css') }} -{% endblock %} - -{% block body %} -{{ block('header') }} -
- {% block main %} - {{ main|raw }} - {% endblock %} -
-{{ block('footer') }} -{% endblock %} - -{% block js %} -{{ block('footer_js') }} -{{ block('header_js') }} -{{ block('core_js') }} -{% endblock %} diff --git a/composer.json b/composer.json index c440e4c..eb5ffb2 100755 --- a/composer.json +++ b/composer.json @@ -2,15 +2,19 @@ "name": "mirzaev/notchat", "description": "Free P2P chat based on asdasd", "readme": "README.md", - "keywords": [], + "keywords": [ + "chat", + "p2p", + "blockchain" + ], "type": "site", "homepage": "https://git.mirzaev.sexy/mirzaev/notchat", "license": "WTFPL", "authors": [ { - "name": "mirzaev", - "email": "mirzaev@gmail.com", - "homepage": "https://mirzaev.page", + "name": "Arsen Mirzaev Tatyano-Muradovich", + "email": "arsen@mirzaev.sexy", + "homepage": "https://mirzaev.sexy", "role": "Programmer" } ], @@ -20,11 +24,8 @@ }, "require": { "php": "~8.3", - "ext-sodium": "~8.3", + "ext-openswoole": "~20230831", "mirzaev/minimal": "^2.2.0", - "mirzaev/accounts": "~1.2.x-dev", - "mirzaev/arangodb": "^1.0.0", - "triagens/arangodb": "~3.9.x-dev", "twig/twig": "^3.4" }, "require-dev": { diff --git a/mirzaev/notchat/system/controllers/core.php b/mirzaev/notchat/system/controllers/core.php new file mode 100644 index 0000000..455c220 --- /dev/null +++ b/mirzaev/notchat/system/controllers/core.php @@ -0,0 +1,72 @@ + + */ +class core extends controller +{ + /** + * Postfix for name of controllers files + */ + final public const POSTFIX = ''; + + /** + * Registry of errors + */ + protected array $errors = []; + + /** + * Constructor of an instance + * + * @param bool $initialize Initialize a controller? + * + * @return void + */ + public function __construct(bool $initialize = true) + { + // For the extends system + parent::__construct($initialize); + + if ($initialize) { + // Initializing is requested + + // Initializing of models core + new models(); + + // Initializing of preprocessor of views + $this->view = new templater(); + } + } + + /** + * Check of initialization + * + * Checks whether a property is initialized in a document instance from ArangoDB + * + * @param string $name Name of the property from ArangoDB + * + * @return bool The property is initialized? + */ + public function __isset(string $name): bool + { + // Check of initialization of the property and exit (success) + return match ($name) { + default => isset($this->{$name}) + }; + } + +} diff --git a/mirzaev/notchat/system/controllers/index.php b/mirzaev/notchat/system/controllers/index.php new file mode 100644 index 0000000..6e96288 --- /dev/null +++ b/mirzaev/notchat/system/controllers/index.php @@ -0,0 +1,32 @@ + + */ +final class index extends core +{ + /** + * Render the main page + * + * @param array $parameters Parameters of the request (POST + GET) + */ + public function index(array $parameters = []): ?string + { + // Exit (success) + if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('chats.html'); + else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $this->view->render('chats.html'); + + // Exit (fail) + return null; + } +} diff --git a/mirzaev/notchat/system/models/core.php b/mirzaev/notchat/system/models/core.php new file mode 100644 index 0000000..86be6fb --- /dev/null +++ b/mirzaev/notchat/system/models/core.php @@ -0,0 +1,111 @@ + + */ +class core extends model +{ + /** + * Postfix for name of models files + */ + final public const POSTFIX = ''; + + /** + * Constructor of an instance + * + * @param bool $initialize Initialize a model? + * + * @return void + */ + public function __construct(bool $initialize = true) + { + // For the extends system + parent::__construct($initialize); + + if ($initialize) { + // Initializing is requested + } + } + + /** + * Write + * + * @param string $name Name of the property + * @param mixed $value Value of the property + * + * @return void + */ + public function __set(string $name, mixed $value = null): void + { + match ($name) { + default => parent::__set($name, $value) + }; + } + + /** + * Read + * + * @param string $name Name of the property + * + * @return mixed Content of the property, if they are found + */ + public function __get(string $name): mixed + { + return match ($name) { + default => parent::__get($name) + }; + } + + /** + * Delete + * + * @param string $name Name of the property + * + * @return void + */ + public function __unset(string $name): void + { + // Deleting a property and exit (success) + parent::__unset($name); + } + + /** + * Check of initialization + * + * @param string $name Name of the property + * + * @return bool The property is initialized? + */ + public function __isset(string $name): bool + { + // Check of initialization of the property and exit (success) + return parent::__isset($name); + } + + /** + * Call a static property or method + * + * @param string $name Name of the property or the method + * @param array $arguments Arguments for the method + */ + public static function __callStatic(string $name, array $arguments): mixed + { + match ($name) { + default => throw new exception("Not found: {$name}", 500) + }; + } +} + diff --git a/mirzaev/notchat/system/public/css/themes/default/main.css b/mirzaev/notchat/system/public/css/themes/default/main.css new file mode 100644 index 0000000..2ca7ed2 --- /dev/null +++ b/mirzaev/notchat/system/public/css/themes/default/main.css @@ -0,0 +1,186 @@ +@charset "UTF-8"; + +:root { + /* --background: #153439; ОЧЕНЬ КРУТОЙ */ + --background: #153439; + --background-2: #153439; + --important: #0b4e51; + --envelope: #28575b; + --section-blue: #0b474a; + --section: #11484a; + --red: #4d2d2d; + --green: #415e53; + --blue: #243b4f; +} + +* { + text-decoration: none; + outline: none; + border: none; + /* font-family: , system-ui, sans-serif; */ + font-family: "dejavu"; + transition: 0.1s ease-out; +} + +body { + --row-aside: 200px; + --row-settings: 100px; + --gap: 16px; + margin: 0; + width: auto; + height: 100vh; + display: grid; + grid-template-columns: [header] 220px [settings] 320px [main] auto [footer] 180px; + grid-template-rows: [aside] var(--row-aside, 200px) [settings] var(--row-settings, 100px) [main] auto; + gap: var(--gap, 16px); + padding: 0; + overflow-x: scroll; + overflow-y: clip; + background-color: var(--background, #fafafa); + --body-rays: inset 0px 8vh 35vw -8vw var(--background-2), inset 0px -8vh 35vw -8vw var(--background-2); + box-shadow: var(--body-rays); + -webkit-box-shadow: var(--body-rays); + -moz-box-shadow: var(--body-rays); +} + +header { + z-index: 1000; + grid-column: header; + grid-row: 1 / -1; + display: grid; + grid-template-rows: inherit; +} + +header>section[data-section="window"] { + z-index: 1100; + grid-row: aside; +} + +header>section[data-section="main"] { + z-index: 1200; + grid-row: settings / -1; + display: grid; + background-color: var(--envelope); +} + +section[data-section="servers"] { + z-index: 250; + grid-row: settings; + background-color: var(--important); + border-radius: 5px; +} + +section[data-section="chats"] { + z-index: 200; + margin-bottom: var(--gap); + grid-row: main; + background-color: var(--section); + border-radius: 5px; +} + +aside { + z-index: 300; + grid-column: 1 / -1; + grid-row: aside; + display: flex; +} + +main { + --sections-default: 1; + --sections-width: 520px; + z-index: 100; + margin-bottom: var(--gap); + grid-row: settings / -1; + grid-column: main; + display: grid; + grid-template-rows: [settings] var(--row-settings, 100px) [main] auto; + grid-template-columns: repeat(var(--sections, var(--sections-default, 1)), [chat] var(--sections-width, 520px)); + flex-direction: column; + align-items: center; + gap: var(--gap, 16px); + transition: 0s; +} + +main>section { + width: 100%; + height: 100%; + max-width: var(--sections-width, 480px); + grid-column: var(--position, 1); + grid-row: main; + overflow-x: crop; + overflow-y: scroll; + background-color: var(--section); + border-radius: 5px; +} + +aside { + z-index: 300; + grid-column: 1 / -1; + grid-row: aside; + display: flex; +} + +main { + --sections-default: 1; + --sections-width: 480px; + z-index: 100; + grid-row: settings / -1; + grid-column: main; + display: grid; + grid-template-rows: [settings] var(--row-settings, 100px) [main] auto; + grid-template-columns: repeat(var(--sections, var(--sections-default, 1)), [chat] var(--sections-width, 480px)); + flex-direction: column; + align-items: center; + gap: var(--gap, 16px); + transition: 0s; +} + +main>section { + width: 100%; + max-width: var(--sections-width, 480px); + grid-column: var(--position, 1); + grid-row: main; + overflow: scroll; + background-color: var(--section); +} + +main>section[data-panel-type="settings"] { + grid-row: settings; +} + +footer { + z-index: 500; + grid-column: footer; + grid-row: 1 / -1; + display: grid; + grid-template-rows: inherit; +} + +footer>section[data-section="window"] { + z-index: 600; + grid-row: aside; +} + +footer>section[data-section="main"] { + z-index: 700; + grid-row: settings / -1; + display: grid; + background-color: var(--envelope); +} + +:is(div, section).window { + overflow: hidden; + border-right: 1px solid; + border-right-color: rgba(174, 122, 122, 0.71); + background-color: rgba(255, 255, 255, 0.27); + backdrop-filter: blur(2px); +} + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/mirzaev/notchat/system/public/index.php b/mirzaev/notchat/system/public/index.php new file mode 100644 index 0000000..5532dcd --- /dev/null +++ b/mirzaev/notchat/system/public/index.php @@ -0,0 +1,42 @@ +write('/', 'index', 'index'); + +// Инициализация ядра +$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false)); + +// Обработка запроса +echo $core->start(); diff --git a/mirzaev/notchat/system/public/js/adapter.js b/mirzaev/notchat/system/public/js/adapter.js new file mode 100644 index 0000000..9085453 --- /dev/null +++ b/mirzaev/notchat/system/public/js/adapter.js @@ -0,0 +1,2653 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? 'm=' + part : part).trim() + '\r\n'; + }); +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parts[1], + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + default: // Unknown extensions are silently ignored. + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); // was: relAddr + sdp.push('rport'); + sdp.push(candidate.relatedPort); // was: relPort + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + return 'candidate:' + sdp.join(' '); +}; + +// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + // was: channels + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + return parsed; +}; + +// Generate an a=rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; +}; + +// Parses an a=extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(' '); + return { + id: parseInt(parts[0], 10), + uri: parts[1] + }; +}; + +// Generates a=extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function(headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + + ' ' + headerExtension.uri + '\r\n'; +}; + +// Parses an ftmp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + params.push(param + '=' + codec.parameters[param]); + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; +}; + +// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; +}; +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' + fb.parameter + + '\r\n'; + }); + } + return lines; +}; + +// Parses an RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var fpLine = lines.filter(function(line) { + return line.indexOf('a=fingerprint:') === 0; + })[0].substr(14); + // Note: a=setup line is ignored since we use the 'auto' role. + var dtlsParameters = { + role: 'auto', + fingerprints: [{ + algorithm: fpLine.split(' ')[0], + value: fpLine.split(' ')[1] + }] + }; + return dtlsParameters; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; +}; +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var iceParameters = { + usernameFragment: lines.filter(function(line) { + return line.indexOf('a=ice-ufrag:') === 0; + })[0].substr(12), + password: lines.filter(function(line) { + return line.indexOf('a=ice-pwd:') === 0; + })[0].substr(10) + }; + return iceParameters; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + // FIXME: add headerExtensions, fecMechanismş and rtcp. + sdp += 'a=rtcp-mux\r\n'; + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') + .map(function(line) { + var parts = line.split(' '); + parts.shift(); + return parts.map(function(part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10), + rtx: { + payloadType: codec.payloadType, + ssrc: secondarySsrc + } + }; + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: secondarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(5), 10); + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +SDPUtils.writeSessionBoilerplate = function() { + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; +}; + +SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. + if (transceiver.rtpSender) { + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + return sdp; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; +}; + +// Expose public methods. +module.exports = SDPUtils; + +},{}],2:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ + +'use strict'; + +// Shimming starts here. +(function() { + // Utils. + var logging = require('./utils').log; + var browserDetails = require('./utils').browserDetails; + // Export to the adapter global object visible in the browser. + module.exports.browserDetails = browserDetails; + module.exports.extractVersion = require('./utils').extractVersion; + module.exports.disableLog = require('./utils').disableLog; + + // Uncomment the line below if you want logging to occur, including logging + // for the switch statement below. Can also be turned on in the browser via + // adapter.disableLog(false), but then logging from the switch statement below + // will not appear. + // require('./utils').disableLog(false); + + // Browser shims. + var chromeShim = require('./chrome/chrome_shim') || null; + var edgeShim = require('./edge/edge_shim') || null; + var firefoxShim = require('./firefox/firefox_shim') || null; + var safariShim = require('./safari/safari_shim') || null; + + // Shim browser if found. + switch (browserDetails.browser) { + case 'opera': // fallthrough as it uses chrome shims + case 'chrome': + if (!chromeShim || !chromeShim.shimPeerConnection) { + logging('Chrome shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming chrome.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = chromeShim; + + chromeShim.shimGetUserMedia(); + chromeShim.shimMediaStream(); + chromeShim.shimSourceObject(); + chromeShim.shimPeerConnection(); + chromeShim.shimOnTrack(); + break; + case 'firefox': + if (!firefoxShim || !firefoxShim.shimPeerConnection) { + logging('Firefox shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming firefox.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = firefoxShim; + + firefoxShim.shimGetUserMedia(); + firefoxShim.shimSourceObject(); + firefoxShim.shimPeerConnection(); + firefoxShim.shimOnTrack(); + break; + case 'edge': + if (!edgeShim || !edgeShim.shimPeerConnection) { + logging('MS edge shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming edge.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = edgeShim; + + edgeShim.shimGetUserMedia(); + edgeShim.shimPeerConnection(); + break; + case 'safari': + if (!safariShim) { + logging('Safari shim is not included in this adapter release.'); + return; + } + logging('adapter.js shimming safari.'); + // Export to the adapter global object visible in the browser. + module.exports.browserShim = safariShim; + + safariShim.shimGetUserMedia(); + break; + default: + logging('Unsupported browser!'); + } +})(); + +},{"./chrome/chrome_shim":3,"./edge/edge_shim":5,"./firefox/firefox_shim":7,"./safari/safari_shim":9,"./utils":10}],3:[function(require,module,exports){ + +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var logging = require('../utils.js').log; +var browserDetails = require('../utils.js').browserDetails; + +var chromeShim = { + shimMediaStream: function() { + window.MediaStream = window.MediaStream || window.webkitMediaStream; + }, + + shimOnTrack: function() { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + var self = this; + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + this.removeEventListener('addstream', this._ontrackpoly); + } + this.addEventListener('track', this._ontrack = f); + this.addEventListener('addstream', this._ontrackpoly = function(e) { + // onaddstream does not fire when a track is added to an existing + // stream. But stream.onaddtrack is implemented so we use that. + e.stream.addEventListener('addtrack', function(te) { + var event = new Event('track'); + event.track = te.track; + event.receiver = {track: te.track}; + event.streams = [e.stream]; + self.dispatchEvent(event); + }); + e.stream.getTracks().forEach(function(track) { + var event = new Event('track'); + event.track = track; + event.receiver = {track: track}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }.bind(this)); + }.bind(this)); + } + }); + } + }, + + shimSourceObject: function() { + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this._srcObject; + }, + set: function(stream) { + var self = this; + // Use _srcObject as a private property for this shim + this._srcObject = stream; + if (this.src) { + URL.revokeObjectURL(this.src); + } + + if (!stream) { + this.src = ''; + return; + } + this.src = URL.createObjectURL(stream); + // We need to recreate the blob url when a track is added or + // removed. Doing it manually since we want to avoid a recursion. + stream.addEventListener('addtrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + stream.addEventListener('removetrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + } + }); + } + } + }, + + shimPeerConnection: function() { + // The RTCPeerConnection object. + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + // Translate iceTransportPolicy to iceTransports, + // see https://code.google.com/p/webrtc/issues/detail?id=4869 + logging('PeerConnection'); + if (pcConfig && pcConfig.iceTransportPolicy) { + pcConfig.iceTransports = pcConfig.iceTransportPolicy; + } + + var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); + var origGetStats = pc.getStats.bind(pc); + pc.getStats = function(selector, successCallback, errorCallback) { + var self = this; + var args = arguments; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats(selector, successCallback); + } + + var fixChromeStats_ = function(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + + // shim getStats with maplike support + var makeMapStats = function(stats, legacyStats) { + var map = new Map(Object.keys(stats).map(function(key) { + return[key, stats[key]]; + })); + legacyStats = legacyStats || stats; + Object.keys(legacyStats).forEach(function(key) { + map[key] = legacyStats[key]; + }); + return map; + }; + + if (arguments.length >= 2) { + var successCallbackWrapper_ = function(response) { + args[1](makeMapStats(fixChromeStats_(response))); + }; + + return origGetStats.apply(this, [successCallbackWrapper_, + arguments[0]]); + } + + // promise-support + return new Promise(function(resolve, reject) { + if (args.length === 1 && typeof selector === 'object') { + origGetStats.apply(self, [ + function(response) { + resolve(makeMapStats(fixChromeStats_(response))); + }, reject]); + } else { + // Preserve legacy chrome stats only on legacy access of stats obj + origGetStats.apply(self, [ + function(response) { + resolve(makeMapStats(fixChromeStats_(response), + response.result())); + }, reject]); + } + }).then(successCallback, errorCallback); + }; + + return pc; + }; + window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype; + + // wrap static methods. Currently just generateCertificate. + if (webkitRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return webkitRTCPeerConnection.generateCertificate; + } + }); + } + + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var self = this; + if (arguments.length < 1 || (arguments.length === 1 && + typeof arguments[0] === 'object')) { + var opts = arguments.length === 1 ? arguments[0] : undefined; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [resolve, reject, opts]); + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + + // add promise support -- natively available in Chrome 51 + if (browserDetails.version < 51) { + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var args = arguments; + var self = this; + var promise = new Promise(function(resolve, reject) { + nativeMethod.apply(self, [args[0], resolve, reject]); + }); + if (args.length < 2) { + return promise; + } + return promise.then(function() { + args[1].apply(null, []); + }, + function(err) { + if (args.length >= 3) { + args[2].apply(null, [err]); + } + }); + }; + }); + } + + // support for addIceCandidate(null) + var nativeAddIceCandidate = + RTCPeerConnection.prototype.addIceCandidate; + RTCPeerConnection.prototype.addIceCandidate = function() { + return arguments[0] === null ? Promise.resolve() + : nativeAddIceCandidate.apply(this, arguments); + }; + + // shim implicit creation of RTCSessionDescription/RTCIceCandidate + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + RTCIceCandidate : RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + }, + + // Attach a media stream to an element. + attachMediaStream: function(element, stream) { + logging('DEPRECATED, attachMediaStream will soon be removed.'); + if (browserDetails.version >= 43) { + element.srcObject = stream; + } else if (typeof element.src !== 'undefined') { + element.src = URL.createObjectURL(stream); + } else { + logging('Error attaching stream to element.'); + } + }, + + reattachMediaStream: function(to, from) { + logging('DEPRECATED, reattachMediaStream will soon be removed.'); + if (browserDetails.version >= 43) { + to.srcObject = from.srcObject; + } else { + to.src = from.src; + } + } +}; + + +// Expose public methods. +module.exports = { + shimMediaStream: chromeShim.shimMediaStream, + shimOnTrack: chromeShim.shimOnTrack, + shimSourceObject: chromeShim.shimSourceObject, + shimPeerConnection: chromeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia'), + attachMediaStream: chromeShim.attachMediaStream, + reattachMediaStream: chromeShim.reattachMediaStream +}; + +},{"../utils.js":10,"./getusermedia":4}],4:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var logging = require('../utils.js').log; + +// Expose public methods. +module.exports = function() { + var constraintsToChrome_ = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname_ = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname_('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname_('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname_('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + var shimConstraints_ = function(constraints, func) { + constraints = JSON.parse(JSON.stringify(constraints)); + if (constraints && constraints.audio) { + constraints.audio = constraintsToChrome_(constraints.audio); + } + if (constraints && typeof constraints.video === 'object') { + // Shim facingMode for mobile, where it defaults to "user". + var face = constraints.video.facingMode; + face = face && ((typeof face === 'object') ? face : {ideal: face}); + + if ((face && (face.exact === 'user' || face.exact === 'environment' || + face.ideal === 'user' || face.ideal === 'environment')) && + !(navigator.mediaDevices.getSupportedConstraints && + navigator.mediaDevices.getSupportedConstraints().facingMode)) { + delete constraints.video.facingMode; + if (face.exact === 'environment' || face.ideal === 'environment') { + // Look for "back" in label, or use last cam (typically back cam). + return navigator.mediaDevices.enumerateDevices() + .then(function(devices) { + devices = devices.filter(function(d) { + return d.kind === 'videoinput'; + }); + var back = devices.find(function(d) { + return d.label.toLowerCase().indexOf('back') !== -1; + }) || (devices.length && devices[devices.length - 1]); + if (back) { + constraints.video.deviceId = face.exact ? {exact: back.deviceId} : + {ideal: back.deviceId}; + } + constraints.video = constraintsToChrome_(constraints.video); + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }); + } + } + constraints.video = constraintsToChrome_(constraints.video); + } + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }; + + var shimError_ = function(e) { + return { + name: { + PermissionDeniedError: 'NotAllowedError', + ConstraintNotSatisfiedError: 'OverconstrainedError' + }[e.name] || e.name, + message: e.message, + constraint: e.constraintName, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + var getUserMedia_ = function(constraints, onSuccess, onError) { + shimConstraints_(constraints, function(c) { + navigator.webkitGetUserMedia(c, onSuccess, function(e) { + onError(shimError_(e)); + }); + }); + }; + + navigator.getUserMedia = getUserMedia_; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + navigator.getUserMedia(constraints, resolve, reject); + }); + }; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = { + getUserMedia: getUserMediaPromise_, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + deviceId: device.id, + groupId: ''}; + })); + }); + }); + } + }; + } + + // A shim for getUserMedia method on the mediaDevices object. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (!navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia = function(constraints) { + return getUserMediaPromise_(constraints); + }; + } else { + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(cs) { + return shimConstraints_(cs, function(c) { + return origGetUserMedia(c).catch(function(e) { + return Promise.reject(shimError_(e)); + }); + }); + }; + } + + // Dummy devicechange event methods. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (typeof navigator.mediaDevices.addEventListener === 'undefined') { + navigator.mediaDevices.addEventListener = function() { + logging('Dummy mediaDevices.addEventListener called.'); + }; + } + if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { + navigator.mediaDevices.removeEventListener = function() { + logging('Dummy mediaDevices.removeEventListener called.'); + }; + } +}; + +},{"../utils.js":10}],5:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var SDPUtils = require('sdp'); +var logging = require('../utils').log; + +var edgeShim = { + shimPeerConnection: function() { + if (window.RTCIceGatherer) { + // ORTC defines an RTCIceCandidate object but no constructor. + // Not implemented in Edge. + if (!window.RTCIceCandidate) { + window.RTCIceCandidate = function(args) { + return args; + }; + } + // ORTC does not have a session description object but + // other browsers (i.e. Chrome) that will support both PC and ORTC + // in the future might have this defined already. + if (!window.RTCSessionDescription) { + window.RTCSessionDescription = function(args) { + return args; + }; + } + } + + window.RTCPeerConnection = function(config) { + var self = this; + + var _eventTarget = document.createDocumentFragment(); + ['addEventListener', 'removeEventListener', 'dispatchEvent'] + .forEach(function(method) { + self[method] = _eventTarget[method].bind(_eventTarget); + }); + + this.onicecandidate = null; + this.onaddstream = null; + this.ontrack = null; + this.onremovestream = null; + this.onsignalingstatechange = null; + this.oniceconnectionstatechange = null; + this.onnegotiationneeded = null; + this.ondatachannel = null; + + this.localStreams = []; + this.remoteStreams = []; + this.getLocalStreams = function() { + return self.localStreams; + }; + this.getRemoteStreams = function() { + return self.remoteStreams; + }; + + this.localDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.remoteDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.signalingState = 'stable'; + this.iceConnectionState = 'new'; + this.iceGatheringState = 'new'; + + this.iceOptions = { + gatherPolicy: 'all', + iceServers: [] + }; + if (config && config.iceTransportPolicy) { + switch (config.iceTransportPolicy) { + case 'all': + case 'relay': + this.iceOptions.gatherPolicy = config.iceTransportPolicy; + break; + case 'none': + // FIXME: remove once implementation and spec have added this. + throw new TypeError('iceTransportPolicy "none" not supported'); + default: + // don't set iceTransportPolicy. + break; + } + } + this.usingBundle = config && config.bundlePolicy === 'max-bundle'; + + if (config && config.iceServers) { + // Edge does not like + // 1) stun: + // 2) turn: that does not have all of turn:host:port?transport=udp + var iceServers = JSON.parse(JSON.stringify(config.iceServers)); + this.iceOptions.iceServers = iceServers.filter(function(server) { + if (server && server.urls) { + var urls = server.urls; + if (typeof urls === 'string') { + urls = [urls]; + } + urls = urls.filter(function(url) { + return url.indexOf('turn:') === 0 && + url.indexOf('transport=udp') !== -1; + })[0]; + return !!urls; + } + return false; + }); + } + + // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... + // everything that is needed to describe a SDP m-line. + this.transceivers = []; + + // since the iceGatherer is currently created in createOffer but we + // must not emit candidates until after setLocalDescription we buffer + // them in this array. + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { + var self = this; + var sections = SDPUtils.splitSections(self.localDescription.sdp); + // FIXME: need to apply ice candidates in a way which is async but + // in-order + this._localIceCandidatesBuffer.forEach(function(event) { + var end = !event.candidate || Object.keys(event.candidate).length === 0; + if (end) { + for (var j = 1; j < sections.length; j++) { + if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) { + sections[j] += 'a=end-of-candidates\r\n'; + } + } + } else if (event.candidate.candidate.indexOf('typ endOfCandidates') + === -1) { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=' + event.candidate.candidate + '\r\n'; + } + self.localDescription.sdp = sections.join(''); + self.dispatchEvent(event); + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + if (!event.candidate && self.iceGatheringState !== 'complete') { + var complete = self.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + if (complete) { + self.iceGatheringState = 'complete'; + } + } + }); + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype.addStream = function(stream) { + // Clone is necessary for local demos mostly, attaching directly + // to two different senders does not work (build 10547). + this.localStreams.push(stream.clone()); + this._maybeFireNegotiationNeeded(); + }; + + window.RTCPeerConnection.prototype.removeStream = function(stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this._maybeFireNegotiationNeeded(); + } + }; + + window.RTCPeerConnection.prototype.getSenders = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpSender; + }) + .map(function(transceiver) { + return transceiver.rtpSender; + }); + }; + + window.RTCPeerConnection.prototype.getReceivers = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpReceiver; + }) + .map(function(transceiver) { + return transceiver.rtpReceiver; + }); + }; + + // Determines the intersection of local and remote capabilities. + window.RTCPeerConnection.prototype._getCommonCapabilities = + function(localCapabilities, remoteCapabilities) { + var commonCapabilities = { + codecs: [], + headerExtensions: [], + fecMechanisms: [] + }; + localCapabilities.codecs.forEach(function(lCodec) { + for (var i = 0; i < remoteCapabilities.codecs.length; i++) { + var rCodec = remoteCapabilities.codecs[i]; + if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && + lCodec.clockRate === rCodec.clockRate && + lCodec.numChannels === rCodec.numChannels) { + // push rCodec so we reply with offerer payload type + commonCapabilities.codecs.push(rCodec); + + // FIXME: also need to determine intersection between + // .rtcpFeedback and .parameters + break; + } + } + }); + + localCapabilities.headerExtensions + .forEach(function(lHeaderExtension) { + for (var i = 0; i < remoteCapabilities.headerExtensions.length; + i++) { + var rHeaderExtension = remoteCapabilities.headerExtensions[i]; + if (lHeaderExtension.uri === rHeaderExtension.uri) { + commonCapabilities.headerExtensions.push(rHeaderExtension); + break; + } + } + }); + + // FIXME: fecMechanisms + return commonCapabilities; + }; + + // Create ICE gatherer, ICE transport and DTLS transport. + window.RTCPeerConnection.prototype._createIceAndDtlsTransports = + function(mid, sdpMLineIndex) { + var self = this; + var iceGatherer = new RTCIceGatherer(self.iceOptions); + var iceTransport = new RTCIceTransport(iceGatherer); + iceGatherer.onlocalcandidate = function(evt) { + var event = new Event('icecandidate'); + event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; + + var cand = evt.candidate; + var end = !cand || Object.keys(cand).length === 0; + // Edge emits an empty object for RTCIceCandidateComplete‥ + if (end) { + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + if (iceGatherer.state === undefined) { + iceGatherer.state = 'completed'; + } + + // Emit a candidate with type endOfCandidates to make the samples + // work. Edge requires addIceCandidate with this empty candidate + // to start checking. The real solution is to signal + // end-of-candidates to the other side when getting the null + // candidate but some apps (like the samples) don't do that. + event.candidate.candidate = + 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'; + } else { + // RTCIceCandidate doesn't have a component, needs to be added + cand.component = iceTransport.component === 'RTCP' ? 2 : 1; + event.candidate.candidate = SDPUtils.writeCandidate(cand); + } + + // update local description. + var sections = SDPUtils.splitSections(self.localDescription.sdp); + if (event.candidate.candidate.indexOf('typ endOfCandidates') + === -1) { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=' + event.candidate.candidate + '\r\n'; + } else { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=end-of-candidates\r\n'; + } + self.localDescription.sdp = sections.join(''); + + var complete = self.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + + // Emit candidate if localDescription is set. + // Also emits null candidate when all gatherers are complete. + switch (self.iceGatheringState) { + case 'new': + self._localIceCandidatesBuffer.push(event); + if (end && complete) { + self._localIceCandidatesBuffer.push( + new Event('icecandidate')); + } + break; + case 'gathering': + self._emitBufferedCandidates(); + self.dispatchEvent(event); + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + if (complete) { + self.dispatchEvent(new Event('icecandidate')); + if (self.onicecandidate !== null) { + self.onicecandidate(new Event('icecandidate')); + } + self.iceGatheringState = 'complete'; + } + break; + case 'complete': + // should not happen... currently! + break; + default: // no-op. + break; + } + }; + iceTransport.onicestatechange = function() { + self._updateConnectionState(); + }; + + var dtlsTransport = new RTCDtlsTransport(iceTransport); + dtlsTransport.ondtlsstatechange = function() { + self._updateConnectionState(); + }; + dtlsTransport.onerror = function() { + // onerror does not set state to failed by itself. + dtlsTransport.state = 'failed'; + self._updateConnectionState(); + }; + + return { + iceGatherer: iceGatherer, + iceTransport: iceTransport, + dtlsTransport: dtlsTransport + }; + }; + + // Start the RTP Sender and Receiver for a transceiver. + window.RTCPeerConnection.prototype._transceive = function(transceiver, + send, recv) { + var params = this._getCommonCapabilities(transceiver.localCapabilities, + transceiver.remoteCapabilities); + if (send && transceiver.rtpSender) { + params.encodings = transceiver.sendEncodingParameters; + params.rtcp = { + cname: SDPUtils.localCName + }; + if (transceiver.recvEncodingParameters.length) { + params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; + } + transceiver.rtpSender.send(params); + } + if (recv && transceiver.rtpReceiver) { + params.encodings = transceiver.recvEncodingParameters; + params.rtcp = { + cname: transceiver.cname + }; + if (transceiver.sendEncodingParameters.length) { + params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; + } + transceiver.rtpReceiver.receive(params); + } + }; + + window.RTCPeerConnection.prototype.setLocalDescription = + function(description) { + var self = this; + var sections; + var sessionpart; + if (description.type === 'offer') { + // FIXME: What was the purpose of this empty if statement? + // if (!this._pendingOffer) { + // } else { + if (this._pendingOffer) { + // VERY limited support for SDP munging. Limited to: + // * changing the order of codecs + sections = SDPUtils.splitSections(description.sdp); + sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var caps = SDPUtils.parseRtpParameters(mediaSection); + self._pendingOffer[sdpMLineIndex].localCapabilities = caps; + }); + this.transceivers = this._pendingOffer; + delete this._pendingOffer; + } + } else if (description.type === 'answer') { + sections = SDPUtils.splitSections(self.remoteDescription.sdp); + sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + sections.forEach(function(mediaSection, sdpMLineIndex) { + var transceiver = self.transceivers[sdpMLineIndex]; + var iceGatherer = transceiver.iceGatherer; + var iceTransport = transceiver.iceTransport; + var dtlsTransport = transceiver.dtlsTransport; + var localCapabilities = transceiver.localCapabilities; + var remoteCapabilities = transceiver.remoteCapabilities; + var rejected = mediaSection.split('\n', 1)[0] + .split(' ', 2)[1] === '0'; + + if (!rejected) { + var remoteIceParameters = SDPUtils.getIceParameters( + mediaSection, sessionpart); + if (isIceLite) { + var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') + .map(function(cand) { + return SDPUtils.parseCandidate(cand); + }) + .filter(function(cand) { + return cand.component === '1'; + }); + // ice-lite only includes host candidates in the SDP so we can + // use setRemoteCandidates (which implies an + // RTCIceCandidateComplete) + if (cands.length) { + iceTransport.setRemoteCandidates(cands); + } + } + var remoteDtlsParameters = SDPUtils.getDtlsParameters( + mediaSection, sessionpart); + if (isIceLite) { + remoteDtlsParameters.role = 'server'; + } + + if (!self.usingBundle || sdpMLineIndex === 0) { + iceTransport.start(iceGatherer, remoteIceParameters, + isIceLite ? 'controlling' : 'controlled'); + dtlsTransport.start(remoteDtlsParameters); + } + + // Calculate intersection of capabilities. + var params = self._getCommonCapabilities(localCapabilities, + remoteCapabilities); + + // Start the RTCRtpSender. The RTCRtpReceiver for this + // transceiver has already been started in setRemoteDescription. + self._transceive(transceiver, + params.codecs.length > 0, + false); + } + }); + } + + this.localDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-local-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + + // If a success callback was provided, emit ICE candidates after it + // has been executed. Otherwise, emit callback after the Promise is + // resolved. + var hasCallback = arguments.length > 1 && + typeof arguments[1] === 'function'; + if (hasCallback) { + var cb = arguments[1]; + window.setTimeout(function() { + cb(); + if (self.iceGatheringState === 'new') { + self.iceGatheringState = 'gathering'; + } + self._emitBufferedCandidates(); + }, 0); + } + var p = Promise.resolve(); + p.then(function() { + if (!hasCallback) { + if (self.iceGatheringState === 'new') { + self.iceGatheringState = 'gathering'; + } + // Usually candidates will be emitted earlier. + window.setTimeout(self._emitBufferedCandidates.bind(self), 500); + } + }); + return p; + }; + + window.RTCPeerConnection.prototype.setRemoteDescription = + function(description) { + var self = this; + var stream = new MediaStream(); + var receiverList = []; + var sections = SDPUtils.splitSections(description.sdp); + var sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + this.usingBundle = SDPUtils.matchPrefix(sessionpart, + 'a=group:BUNDLE ').length > 0; + sections.forEach(function(mediaSection, sdpMLineIndex) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].substr(2).split(' '); + var kind = mline[0]; + var rejected = mline[1] === '0'; + var direction = SDPUtils.getDirection(mediaSection, sessionpart); + + var transceiver; + var iceGatherer; + var iceTransport; + var dtlsTransport; + var rtpSender; + var rtpReceiver; + var sendEncodingParameters; + var recvEncodingParameters; + var localCapabilities; + + var track; + // FIXME: ensure the mediaSection has rtcp-mux set. + var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); + var remoteIceParameters; + var remoteDtlsParameters; + if (!rejected) { + remoteIceParameters = SDPUtils.getIceParameters(mediaSection, + sessionpart); + remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, + sessionpart); + remoteDtlsParameters.role = 'client'; + } + recvEncodingParameters = + SDPUtils.parseRtpEncodingParameters(mediaSection); + + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:'); + if (mid.length) { + mid = mid[0].substr(6); + } else { + mid = SDPUtils.generateIdentifier(); + } + + var cname; + // Gets the first SSRC. Note that with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + cname = remoteSsrc.value; + } + + var isComplete = SDPUtils.matchPrefix(mediaSection, + 'a=end-of-candidates').length > 0; + var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') + .map(function(cand) { + return SDPUtils.parseCandidate(cand); + }) + .filter(function(cand) { + return cand.component === '1'; + }); + if (description.type === 'offer' && !rejected) { + var transports = self.usingBundle && sdpMLineIndex > 0 ? { + iceGatherer: self.transceivers[0].iceGatherer, + iceTransport: self.transceivers[0].iceTransport, + dtlsTransport: self.transceivers[0].dtlsTransport + } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + if (isComplete) { + transports.iceTransport.setRemoteCandidates(cands); + } + + localCapabilities = RTCRtpReceiver.getCapabilities(kind); + sendEncodingParameters = [{ + ssrc: (2 * sdpMLineIndex + 2) * 1001 + }]; + + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); + + track = rtpReceiver.track; + receiverList.push([track, rtpReceiver]); + // FIXME: not correct when there are multiple streams but that is + // not currently supported in this shim. + stream.addTrack(track); + + // FIXME: look at direction. + if (self.localStreams.length > 0 && + self.localStreams[0].getTracks().length >= sdpMLineIndex) { + // FIXME: actually more complicated, needs to match types etc + var localtrack = self.localStreams[0] + .getTracks()[sdpMLineIndex]; + rtpSender = new RTCRtpSender(localtrack, + transports.dtlsTransport); + } + + self.transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: remoteCapabilities, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + cname: cname, + sendEncodingParameters: sendEncodingParameters, + recvEncodingParameters: recvEncodingParameters + }; + // Start the RTCRtpReceiver now. The RTPSender is started in + // setLocalDescription. + self._transceive(self.transceivers[sdpMLineIndex], + false, + direction === 'sendrecv' || direction === 'sendonly'); + } else if (description.type === 'answer' && !rejected) { + transceiver = self.transceivers[sdpMLineIndex]; + iceGatherer = transceiver.iceGatherer; + iceTransport = transceiver.iceTransport; + dtlsTransport = transceiver.dtlsTransport; + rtpSender = transceiver.rtpSender; + rtpReceiver = transceiver.rtpReceiver; + sendEncodingParameters = transceiver.sendEncodingParameters; + localCapabilities = transceiver.localCapabilities; + + self.transceivers[sdpMLineIndex].recvEncodingParameters = + recvEncodingParameters; + self.transceivers[sdpMLineIndex].remoteCapabilities = + remoteCapabilities; + self.transceivers[sdpMLineIndex].cname = cname; + + if ((isIceLite || isComplete) && cands.length) { + iceTransport.setRemoteCandidates(cands); + } + if (!self.usingBundle || sdpMLineIndex === 0) { + iceTransport.start(iceGatherer, remoteIceParameters, + 'controlling'); + dtlsTransport.start(remoteDtlsParameters); + } + + self._transceive(transceiver, + direction === 'sendrecv' || direction === 'recvonly', + direction === 'sendrecv' || direction === 'sendonly'); + + if (rtpReceiver && + (direction === 'sendrecv' || direction === 'sendonly')) { + track = rtpReceiver.track; + receiverList.push([track, rtpReceiver]); + stream.addTrack(track); + } else { + // FIXME: actually the receiver should be created later. + delete transceiver.rtpReceiver; + } + } + }); + + this.remoteDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-remote-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + if (stream.getTracks().length) { + self.remoteStreams.push(stream); + window.setTimeout(function() { + var event = new Event('addstream'); + event.stream = stream; + self.dispatchEvent(event); + if (self.onaddstream !== null) { + window.setTimeout(function() { + self.onaddstream(event); + }, 0); + } + + receiverList.forEach(function(item) { + var track = item[0]; + var receiver = item[1]; + var trackEvent = new Event('track'); + trackEvent.track = track; + trackEvent.receiver = receiver; + trackEvent.streams = [stream]; + self.dispatchEvent(event); + if (self.ontrack !== null) { + window.setTimeout(function() { + self.ontrack(trackEvent); + }, 0); + } + }); + }, 0); + } + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.close = function() { + this.transceivers.forEach(function(transceiver) { + /* not yet + if (transceiver.iceGatherer) { + transceiver.iceGatherer.close(); + } + */ + if (transceiver.iceTransport) { + transceiver.iceTransport.stop(); + } + if (transceiver.dtlsTransport) { + transceiver.dtlsTransport.stop(); + } + if (transceiver.rtpSender) { + transceiver.rtpSender.stop(); + } + if (transceiver.rtpReceiver) { + transceiver.rtpReceiver.stop(); + } + }); + // FIXME: clean up tracks, local streams, remote streams, etc + this._updateSignalingState('closed'); + }; + + // Update the signaling state. + window.RTCPeerConnection.prototype._updateSignalingState = + function(newState) { + this.signalingState = newState; + var event = new Event('signalingstatechange'); + this.dispatchEvent(event); + if (this.onsignalingstatechange !== null) { + this.onsignalingstatechange(event); + } + }; + + // Determine whether to fire the negotiationneeded event. + window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = + function() { + // Fire away (for now). + var event = new Event('negotiationneeded'); + this.dispatchEvent(event); + if (this.onnegotiationneeded !== null) { + this.onnegotiationneeded(event); + } + }; + + // Update the connection state. + window.RTCPeerConnection.prototype._updateConnectionState = function() { + var self = this; + var newState; + var states = { + 'new': 0, + closed: 0, + connecting: 0, + checking: 0, + connected: 0, + completed: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + states[transceiver.iceTransport.state]++; + states[transceiver.dtlsTransport.state]++; + }); + // ICETransport.completed and connected are the same for this purpose. + states.connected += states.completed; + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.connecting > 0 || states.checking > 0) { + newState = 'connecting'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connected > 0 || states.completed > 0) { + newState = 'connected'; + } + + if (newState !== self.iceConnectionState) { + self.iceConnectionState = newState; + var event = new Event('iceconnectionstatechange'); + this.dispatchEvent(event); + if (this.oniceconnectionstatechange !== null) { + this.oniceconnectionstatechange(event); + } + } + }; + + window.RTCPeerConnection.prototype.createOffer = function() { + var self = this; + if (this._pendingOffer) { + throw new Error('createOffer called while there is a pending offer.'); + } + var offerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + offerOptions = arguments[0]; + } else if (arguments.length === 3) { + offerOptions = arguments[2]; + } + + var tracks = []; + var numAudioTracks = 0; + var numVideoTracks = 0; + // Default to sendrecv. + if (this.localStreams.length) { + numAudioTracks = this.localStreams[0].getAudioTracks().length; + numVideoTracks = this.localStreams[0].getVideoTracks().length; + } + // Determine number of audio and video tracks we need to send/recv. + if (offerOptions) { + // Reject Chrome legacy constraints. + if (offerOptions.mandatory || offerOptions.optional) { + throw new TypeError( + 'Legacy mandatory/optional constraints not supported.'); + } + if (offerOptions.offerToReceiveAudio !== undefined) { + numAudioTracks = offerOptions.offerToReceiveAudio; + } + if (offerOptions.offerToReceiveVideo !== undefined) { + numVideoTracks = offerOptions.offerToReceiveVideo; + } + } + if (this.localStreams.length) { + // Push local streams. + this.localStreams[0].getTracks().forEach(function(track) { + tracks.push({ + kind: track.kind, + track: track, + wantReceive: track.kind === 'audio' ? + numAudioTracks > 0 : numVideoTracks > 0 + }); + if (track.kind === 'audio') { + numAudioTracks--; + } else if (track.kind === 'video') { + numVideoTracks--; + } + }); + } + // Create M-lines for recvonly streams. + while (numAudioTracks > 0 || numVideoTracks > 0) { + if (numAudioTracks > 0) { + tracks.push({ + kind: 'audio', + wantReceive: true + }); + numAudioTracks--; + } + if (numVideoTracks > 0) { + tracks.push({ + kind: 'video', + wantReceive: true + }); + numVideoTracks--; + } + } + + var sdp = SDPUtils.writeSessionBoilerplate(); + var transceivers = []; + tracks.forEach(function(mline, sdpMLineIndex) { + // For each track, create an ice gatherer, ice transport, + // dtls transport, potentially rtpsender and rtpreceiver. + var track = mline.track; + var kind = mline.kind; + var mid = SDPUtils.generateIdentifier(); + + var transports = self.usingBundle && sdpMLineIndex > 0 ? { + iceGatherer: transceivers[0].iceGatherer, + iceTransport: transceivers[0].iceTransport, + dtlsTransport: transceivers[0].dtlsTransport + } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + var localCapabilities = RTCRtpSender.getCapabilities(kind); + var rtpSender; + var rtpReceiver; + + // generate an ssrc now, to be used later in rtpSender.send + var sendEncodingParameters = [{ + ssrc: (2 * sdpMLineIndex + 1) * 1001 + }]; + if (track) { + rtpSender = new RTCRtpSender(track, transports.dtlsTransport); + } + + if (mline.wantReceive) { + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); + } + + transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: null, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + sendEncodingParameters: sendEncodingParameters, + recvEncodingParameters: null + }; + }); + if (this.usingBundle) { + sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + tracks.forEach(function(mline, sdpMLineIndex) { + var transceiver = transceivers[sdpMLineIndex]; + sdp += SDPUtils.writeMediaSection(transceiver, + transceiver.localCapabilities, 'offer', self.localStreams[0]); + }); + + this._pendingOffer = transceivers; + var desc = new RTCSessionDescription({ + type: 'offer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.createAnswer = function() { + var self = this; + + var sdp = SDPUtils.writeSessionBoilerplate(); + if (this.usingBundle) { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + this.transceivers.forEach(function(transceiver) { + // Calculate intersection of capabilities. + var commonCapabilities = self._getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, + 'answer', self.localStreams[0]); + }); + + var desc = new RTCSessionDescription({ + type: 'answer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { + if (candidate === null) { + this.transceivers.forEach(function(transceiver) { + transceiver.iceTransport.addRemoteCandidate({}); + }); + } else { + var mLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < this.transceivers.length; i++) { + if (this.transceivers[i].mid === candidate.sdpMid) { + mLineIndex = i; + break; + } + } + } + var transceiver = this.transceivers[mLineIndex]; + if (transceiver) { + var cand = Object.keys(candidate.candidate).length > 0 ? + SDPUtils.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && cand.port === 0) { + return; + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component !== '1') { + return; + } + // A dirty hack to make samples work. + if (cand.type === 'endOfCandidates') { + cand = {}; + } + transceiver.iceTransport.addRemoteCandidate(cand); + + // update the remoteDescription. + var sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim() + : 'a=end-of-candidates') + '\r\n'; + this.remoteDescription.sdp = sections.join(''); + } + } + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.getStats = function() { + var promises = []; + this.transceivers.forEach(function(transceiver) { + ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', + 'dtlsTransport'].forEach(function(method) { + if (transceiver[method]) { + promises.push(transceiver[method].getStats()); + } + }); + }); + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + return new Promise(function(resolve) { + // shim getStats with maplike support + var results = new Map(); + Promise.all(promises).then(function(res) { + res.forEach(function(result) { + Object.keys(result).forEach(function(id) { + results.set(id, result[id]); + results[id] = result[id]; + }); + }); + if (cb) { + window.setTimeout(cb, 0, results); + } + resolve(results); + }); + }); + }; + }, + + // Attach a media stream to an element. + attachMediaStream: function(element, stream) { + logging('DEPRECATED, attachMediaStream will soon be removed.'); + element.srcObject = stream; + }, + + reattachMediaStream: function(to, from) { + logging('DEPRECATED, reattachMediaStream will soon be removed.'); + to.srcObject = from.srcObject; + } +}; + +// Expose public methods. +module.exports = { + shimPeerConnection: edgeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia'), + attachMediaStream: edgeShim.attachMediaStream, + reattachMediaStream: edgeShim.reattachMediaStream +}; + +},{"../utils":10,"./getusermedia":6,"sdp":1}],6:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +// Expose public methods. +module.exports = function() { + var shimError_ = function(e) { + return { + name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, + message: e.message, + constraint: e.constraint, + toString: function() { + return this.name; + } + }; + }; + + // getUserMedia error shim. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).catch(function(e) { + return Promise.reject(shimError_(e)); + }); + }; +}; + +},{}],7:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logging = require('../utils').log; +var browserDetails = require('../utils').browserDetails; + +var firefoxShim = { + shimOnTrack: function() { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + this.removeEventListener('addstream', this._ontrackpoly); + } + this.addEventListener('track', this._ontrack = f); + this.addEventListener('addstream', this._ontrackpoly = function(e) { + e.stream.getTracks().forEach(function(track) { + var event = new Event('track'); + event.track = track; + event.receiver = {track: track}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }.bind(this)); + }.bind(this)); + } + }); + } + }, + + shimSourceObject: function() { + // Firefox has supported mozSrcObject since FF22, unprefixed in 42. + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this.mozSrcObject; + }, + set: function(stream) { + this.mozSrcObject = stream; + } + }); + } + } + }, + + shimPeerConnection: function() { + if (typeof window !== 'object' || !(window.RTCPeerConnection || + window.mozRTCPeerConnection)) { + return; // probably media.peerconnection.enabled=false in about:config + } + // The RTCPeerConnection object. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (browserDetails.version < 38) { + // .urls is not supported in FF < 38. + // create RTCIceServers with a single url. + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.hasOwnProperty('urls')) { + for (var j = 0; j < server.urls.length; j++) { + var newServer = { + url: server.urls[j] + }; + if (server.urls[j].indexOf('turn') === 0) { + newServer.username = server.username; + newServer.credential = server.credential; + } + newIceServers.push(newServer); + } + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + } + return new mozRTCPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype; + + // wrap static methods. Currently just generateCertificate. + if (mozRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return mozRTCPeerConnection.generateCertificate; + } + }); + } + + window.RTCSessionDescription = mozRTCSessionDescription; + window.RTCIceCandidate = mozRTCIceCandidate; + } + + // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + RTCIceCandidate : RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + + // support for addIceCandidate(null) + var nativeAddIceCandidate = + RTCPeerConnection.prototype.addIceCandidate; + RTCPeerConnection.prototype.addIceCandidate = function() { + return arguments[0] === null ? Promise.resolve() + : nativeAddIceCandidate.apply(this, arguments); + }; + + // shim getStats with maplike support + var makeMapStats = function(stats) { + var map = new Map(); + Object.keys(stats).forEach(function(key) { + map.set(key, stats[key]); + map[key] = stats[key]; + }); + return map; + }; + + var nativeGetStats = RTCPeerConnection.prototype.getStats; + RTCPeerConnection.prototype.getStats = function(selector, onSucc, onErr) { + return nativeGetStats.apply(this, [selector || null]) + .then(function(stats) { + return makeMapStats(stats); + }) + .then(onSucc, onErr); + }; + }, + + // Attach a media stream to an element. + attachMediaStream: function(element, stream) { + logging('DEPRECATED, attachMediaStream will soon be removed.'); + element.srcObject = stream; + }, + + reattachMediaStream: function(to, from) { + logging('DEPRECATED, reattachMediaStream will soon be removed.'); + to.srcObject = from.srcObject; + } +}; + +// Expose public methods. +module.exports = { + shimOnTrack: firefoxShim.shimOnTrack, + shimSourceObject: firefoxShim.shimSourceObject, + shimPeerConnection: firefoxShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia'), + attachMediaStream: firefoxShim.attachMediaStream, + reattachMediaStream: firefoxShim.reattachMediaStream +}; + +},{"../utils":10,"./getusermedia":8}],8:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logging = require('../utils').log; +var browserDetails = require('../utils').browserDetails; + +// Expose public methods. +module.exports = function() { + var shimError_ = function(e) { + return { + name: { + SecurityError: 'NotAllowedError', + PermissionDeniedError: 'NotAllowedError' + }[e.name] || e.name, + message: { + 'The operation is insecure.': 'The request is not allowed by the ' + + 'user agent or the platform in the current context.' + }[e.message] || e.message, + constraint: e.constraint, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + // getUserMedia constraints shim. + var getUserMedia_ = function(constraints, onSuccess, onError) { + var constraintsToFF37_ = function(c) { + if (typeof c !== 'object' || c.require) { + return c; + } + var require = []; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = c[key] = (typeof c[key] === 'object') ? + c[key] : {ideal: c[key]}; + if (r.min !== undefined || + r.max !== undefined || r.exact !== undefined) { + require.push(key); + } + if (r.exact !== undefined) { + if (typeof r.exact === 'number') { + r. min = r.max = r.exact; + } else { + c[key] = r.exact; + } + delete r.exact; + } + if (r.ideal !== undefined) { + c.advanced = c.advanced || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[key] = {min: r.ideal, max: r.ideal}; + } else { + oc[key] = r.ideal; + } + c.advanced.push(oc); + delete r.ideal; + if (!Object.keys(r).length) { + delete c[key]; + } + } + }); + if (require.length) { + c.require = require; + } + return c; + }; + constraints = JSON.parse(JSON.stringify(constraints)); + if (browserDetails.version < 38) { + logging('spec: ' + JSON.stringify(constraints)); + if (constraints.audio) { + constraints.audio = constraintsToFF37_(constraints.audio); + } + if (constraints.video) { + constraints.video = constraintsToFF37_(constraints.video); + } + logging('ff37: ' + JSON.stringify(constraints)); + } + return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { + onError(shimError_(e)); + }); + }; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia_(constraints, resolve, reject); + }); + }; + + // Shim for mediaDevices on older versions. + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, + addEventListener: function() { }, + removeEventListener: function() { } + }; + } + navigator.mediaDevices.enumerateDevices = + navigator.mediaDevices.enumerateDevices || function() { + return new Promise(function(resolve) { + var infos = [ + {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, + {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} + ]; + resolve(infos); + }); + }; + + if (browserDetails.version < 41) { + // Work around http://bugzil.la/1169665 + var orgEnumerateDevices = + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); + navigator.mediaDevices.enumerateDevices = function() { + return orgEnumerateDevices().then(undefined, function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } + if (browserDetails.version < 49) { + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).catch(function(e) { + return Promise.reject(shimError_(e)); + }); + }; + } + navigator.getUserMedia = function(constraints, onSuccess, onError) { + if (browserDetails.version < 44) { + return getUserMedia_(constraints, onSuccess, onError); + } + // Replace Firefox 44+'s deprecation warning with unprefixed version. + console.warn('navigator.getUserMedia has been replaced by ' + + 'navigator.mediaDevices.getUserMedia'); + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); + }; +}; + +},{"../utils":10}],9:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +'use strict'; +var safariShim = { + // TODO: DrAlex, should be here, double check against LayoutTests + // shimOnTrack: function() { }, + + // TODO: DrAlex + // attachMediaStream: function(element, stream) { }, + // reattachMediaStream: function(to, from) { }, + + // TODO: once the back-end for the mac port is done, add. + // TODO: check for webkitGTK+ + // shimPeerConnection: function() { }, + + shimGetUserMedia: function() { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } +}; + +// Expose public methods. +module.exports = { + shimGetUserMedia: safariShim.shimGetUserMedia + // TODO + // shimOnTrack: safariShim.shimOnTrack, + // shimPeerConnection: safariShim.shimPeerConnection, + // attachMediaStream: safariShim.attachMediaStream, + // reattachMediaStream: safariShim.reattachMediaStream +}; + +},{}],10:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logDisabled_ = true; + +// Utility methods. +var utils = { + disableLog: function(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + logDisabled_ = bool; + return (bool) ? 'adapter.js logging disabled' : + 'adapter.js logging enabled'; + }, + + log: function() { + if (typeof window === 'object') { + if (logDisabled_) { + return; + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + } + }, + + /** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ + extractVersion: function(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + }, + + /** + * Browser detector. + * + * @return {object} result containing browser, version and minVersion + * properties. + */ + detectBrowser: function() { + // Returned result object. + var result = {}; + result.browser = null; + result.version = null; + result.minVersion = null; + + // Fail early if it's not a browser + if (typeof window === 'undefined' || !window.navigator) { + result.browser = 'Not a browser.'; + return result; + } + + // Firefox. + if (navigator.mozGetUserMedia) { + result.browser = 'firefox'; + result.version = this.extractVersion(navigator.userAgent, + /Firefox\/([0-9]+)\./, 1); + result.minVersion = 31; + + // all webkit-based browsers + } else if (navigator.webkitGetUserMedia) { + // Chrome, Chromium, Webview, Opera, all use the chrome shim for now + if (window.webkitRTCPeerConnection) { + result.browser = 'chrome'; + result.version = this.extractVersion(navigator.userAgent, + /Chrom(e|ium)\/([0-9]+)\./, 2); + result.minVersion = 38; + + // Safari or unknown webkit-based + // for the time being Safari has support for MediaStreams but not webRTC + } else { + // Safari UA substrings of interest for reference: + // - webkit version: AppleWebKit/602.1.25 (also used in Op,Cr) + // - safari UI version: Version/9.0.3 (unique to Safari) + // - safari UI webkit version: Safari/601.4.4 (also used in Op,Cr) + // + // if the webkit version and safari UI webkit versions are equals, + // ... this is a stable version. + // + // only the internal webkit version is important today to know if + // media streams are supported + // + if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/([0-9]+)\./, 1); + result.minVersion = 602; + + // unknown webkit-based browser + } else { + result.browser = 'Unsupported webkit-based browser ' + + 'with GUM support but no WebRTC support.'; + return result; + } + } + + // Edge. + } else if (navigator.mediaDevices && + navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { + result.browser = 'edge'; + result.version = this.extractVersion(navigator.userAgent, + /Edge\/(\d+).(\d+)$/, 2); + result.minVersion = 10547; + + // Default fallthrough: not supported. + } else { + result.browser = 'Not a supported browser.'; + return result; + } + + // Warn if version is less than minVersion. + if (result.version < result.minVersion) { + utils.log('Browser: ' + result.browser + ' Version: ' + result.version + + ' < minimum supported version: ' + result.minVersion + + '\n some things might not work!'); + } + + return result; + } +}; + +// Export. +module.exports = { + log: utils.log, + disableLog: utils.disableLog, + browserDetails: utils.detectBrowser(), + extractVersion: utils.extractVersion +}; + +},{}]},{},[2])(2) +}); diff --git a/mirzaev/notchat/system/public/js/asdasd b/mirzaev/notchat/system/public/js/asdasd new file mode 120000 index 0000000..dd4d080 --- /dev/null +++ b/mirzaev/notchat/system/public/js/asdasd @@ -0,0 +1 @@ +../../../../../asdasd/system \ No newline at end of file diff --git a/mirzaev/notchat/system/public/js/notchat.js b/mirzaev/notchat/system/public/js/notchat.js new file mode 100644 index 0000000..0d47c81 --- /dev/null +++ b/mirzaev/notchat/system/public/js/notchat.js @@ -0,0 +1,677 @@ +// WebSocket and WebRTC based multi-user chat sample with two-way video +// calling, including use of TURN if applicable or necessary. +// +// This file contains the JavaScript code that implements the client-side +// features for connecting and managing chat and video calls. +// +// To read about how this sample works: http://bit.ly/webrtc-from-chat +// +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Get our hostname + +var myHostname = window.location.hostname; +if (!myHostname) { + myHostname = "localhost"; +} +log("Hostname: " + myHostname); + +// WebSocket chat/signaling channel variables. + +var connection = null; +var clientID = 0; + +// The media constraints object describes what sort of stream we want +// to request from the local A/V hardware (typically a webcam and +// microphone). Here, we specify only that we want both audio and +// video; however, you can be more specific. It's possible to state +// that you would prefer (or require) specific resolutions of video, +// whether to prefer the user-facing or rear-facing camera (if available), +// and so on. +// +// See also: +// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints +// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia +// + +var mediaConstraints = { + audio: true, // We want an audio track + video: { + aspectRatio: { + ideal: 1.333333 // 3:2 aspect is preferred + } + } +}; + +var myUsername = null; +var targetUsername = null; // To store username of other peer +var myPeerConnection = null; // RTCPeerConnection +var transceiver = null; // RTCRtpTransceiver +var webcamStream = null; // MediaStream from webcam + +// Output logging information to console. + +function log(text) { + var time = new Date(); + + console.log("[" + time.toLocaleTimeString() + "] " + text); +} + +// Output an error message to console. + +function log_error(text) { + var time = new Date(); + + console.trace("[" + time.toLocaleTimeString() + "] " + text); +} + +// Send a JavaScript object by converting it to JSON and sending +// it as a message on the WebSocket connection. + +function sendToServer(msg) { + var msgJSON = JSON.stringify(msg); + + log("Sending '" + msg.type + "' message: " + msgJSON); + connection.send(msgJSON); +} + +// Called when the "id" message is received; this message is sent by the +// server to assign this login session a unique ID number; in response, +// this function sends a "username" message to set our username for this +// session. +function setUsername() { + myUsername = document.getElementById("name").value; + + sendToServer({ + name: myUsername, + date: Date.now(), + id: clientID, + type: "username" + }); +} + +// Open and configure the connection to the WebSocket server. + +function connect() { + var serverUrl; + var scheme = "ws"; + + // If this is an HTTPS connection, we have to use a secure WebSocket + // connection too, so add another "s" to the scheme. + + if (document.location.protocol === "https:") { + scheme += "s"; + } + serverUrl = scheme + "://" + 'pantry.bebra.team:6503'; + // serverUrl = scheme + "://" + '95.188.77.71:6503'; + + log(`Connecting to server: ${serverUrl}`); + connection = new WebSocket(serverUrl, "json"); + + connection.onopen = function(evt) { + document.getElementById("text").disabled = false; + document.getElementById("send").disabled = false; + }; + + connection.onerror = function(evt) { + console.dir(evt); + } + + connection.onmessage = function(evt) { + var chatBox = document.querySelector(".chatbox"); + var text = ""; + var msg = JSON.parse(evt.data); + log("Message received: "); + console.dir(msg); + var time = new Date(msg.date); + var timeStr = time.toLocaleTimeString(); + + switch(msg.type) { + case "id": + clientID = msg.id; + setUsername(); + break; + + case "username": + text = "User " + msg.name + " signed in at " + timeStr + "
"; + break; + + case "message": + text = "(" + timeStr + ") " + msg.name + ": " + msg.text + "
"; + break; + + case "rejectusername": + myUsername = msg.name; + text = "Your username has been set to " + myUsername + + " because the name you chose is in use.
"; + break; + + case "userlist": // Received an updated user list + handleUserlistMsg(msg); + break; + + // Signaling messages: these messages are used to trade WebRTC + // signaling information during negotiations leading up to a video + // call. + + case "video-offer": // Invitation and offer to chat + handleVideoOfferMsg(msg); + break; + + case "video-answer": // Callee has answered our offer + handleVideoAnswerMsg(msg); + break; + + case "new-ice-candidate": // A new ICE candidate has been received + handleNewICECandidateMsg(msg); + break; + + case "hang-up": // The other peer has hung up the call + handleHangUpMsg(msg); + break; + + // Unknown message; output to console for debugging. + + default: + log_error("Unknown message received:"); + log_error(msg); + } + + // If there's text to insert into the chat buffer, do so now, then + // scroll the chat panel so that the new text is visible. + + if (text.length) { + chatBox.innerHTML += text; + chatBox.scrollTop = chatBox.scrollHeight - chatBox.clientHeight; + } + }; +} + +// Handles a click on the Send button (or pressing return/enter) by +// building a "message" object and sending it to the server. +function handleSendButton() { + var msg = { + text: document.getElementById("text").value, + type: "message", + id: clientID, + date: Date.now() + }; + sendToServer(msg); + document.getElementById("text").value = ""; +} + +// Handler for keyboard events. This is used to intercept the return and +// enter keys so that we can call send() to transmit the entered text +// to the server. +function handleKey(evt) { + if (evt.keyCode === 13 || evt.keyCode === 14) { + if (!document.getElementById("send").disabled) { + handleSendButton(); + } + } +} + +// Create the RTCPeerConnection which knows how to talk to our +// selected STUN/TURN server and then uses getUserMedia() to find +// our camera and microphone and add that stream to the connection for +// use in our video call. Then we configure event handlers to get +// needed notifications on the call. + +async function createPeerConnection() { + log("Setting up a connection..."); + + // Create an RTCPeerConnection which knows to use our chosen + // STUN server. + + myPeerConnection = new RTCPeerConnection({ + iceServers: [ // Information about ICE servers - Use your own! + { + urls: "turn:" + myHostname, // A TURN server + username: "webrtc", + credential: "turnserver" + } + ] + }); + + // Set up event handlers for the ICE negotiation process. + + myPeerConnection.onicecandidate = handleICECandidateEvent; + myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent; + myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent; + myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; + myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent; + myPeerConnection.ontrack = handleTrackEvent; +} + +// Called by the WebRTC layer to let us know when it's time to +// begin, resume, or restart ICE negotiation. + +async function handleNegotiationNeededEvent() { + log("*** Negotiation needed"); + + try { + log("---> Creating offer"); + const offer = await myPeerConnection.createOffer(); + + // If the connection hasn't yet achieved the "stable" state, + // return to the caller. Another negotiationneeded event + // will be fired when the state stabilizes. + + if (myPeerConnection.signalingState != "stable") { + log(" -- The connection isn't stable yet; postponing...") + return; + } + + // Establish the offer as the local peer's current + // description. + + log("---> Setting local description to the offer"); + await myPeerConnection.setLocalDescription(offer); + + // Send the offer to the remote peer. + + log("---> Sending the offer to the remote peer"); + sendToServer({ + name: myUsername, + target: targetUsername, + type: "video-offer", + sdp: myPeerConnection.localDescription + }); + } catch(err) { + log("*** The following error occurred while handling the negotiationneeded event:"); + reportError(err); + }; +} + +// Called by the WebRTC layer when events occur on the media tracks +// on our WebRTC call. This includes when streams are added to and +// removed from the call. +// +// track events include the following fields: +// +// RTCRtpReceiver receiver +// MediaStreamTrack track +// MediaStream[] streams +// RTCRtpTransceiver transceiver +// +// In our case, we're just taking the first stream found and attaching +// it to the